/*******************************************************************************
 * Copyright (c) 2000, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.core.internal.runtime.auth;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.util.Random;

/**
 * <P>Encrypts or decrypts a sequence of bytes. The bytes are decrypted
 * by supplying the same password that was given when the bytes were
 * encrypted.
 * <P>Here is an example showing how to encrypt and then decrypt the
 * string "Hello, world!" using the password "music":
 * <pre>
 *     String password = "music";
 *     byte[] data = "Hello, world!".getBytes("UTF8");
 *
 *     // Encrypt
 *     Cipher cipher = new Cipher(ENCRYPT_MODE, password);
 *     byte[] encrypted = cipher.cipher(data);
 *
 *     // Decrypt
 *     cipher = new Cipher(DECRYPT_MODE, password);
 *     byte[] decrypted = cipher.cipher(encrypted);
 * </pre>
 */
public class Cipher {
	public static final int DECRYPT_MODE = -1;
	public static final int ENCRYPT_MODE = 1;
	private static final int RANDOM_SIZE = 16;

	private int mode = 0;
	private byte[] password = null;

	//the following fields are used for generating a secure byte stream
	//used by the decryption algorithm
	private byte[] byteStream;
	private int byteStreamOffset;
	private MessageDigest digest;
	private Random random;
	private final byte[] toDigest;

	/**
	 * Initializes the cipher with the given mode and password. This method
	 * must be called first (before any encryption of decryption takes
	 * place) to specify whether the cipher should be in encrypt or decrypt
	 * mode and to set the password.
	 *
	 * @param mode
	 * @param passwordString
	 */
	public Cipher(int mode, String passwordString) {
		this.mode = mode;
		try {
			this.password = passwordString.getBytes("UTF8"); //$NON-NLS-1$
		} catch (UnsupportedEncodingException e) {
			this.password = passwordString.getBytes();
		}
		toDigest = new byte[password.length + RANDOM_SIZE];
	}

	/**
	 * Encrypts or decrypts (depending on which mode the cipher is in) the
	 * given data and returns the result.
	 *
	 * @param data
	 * @return     the result of encrypting or decrypting the given data
	 */
	public byte[] cipher(byte[] data) throws Exception {
		return transform(data, 0, data.length, mode);
	}

	/**
	 * Encrypts or decrypts (depending on which mode the cipher is in) the
	 * given data and returns the result.
	 *
	 * @param data the byte array containg the given data
	 * @param off  the index of the first byte in the given byte array
	 *   to be transformed
	 * @param len  the number of bytes to be transformed
	 * @return the result of encrypting or decrypting the given data
	 */
	public byte[] cipher(byte[] data, int off, int len) throws Exception {
		return transform(data, off, len, mode);
	}

	/**
	 * Encrypts or decrypts (depending on which mode the cipher is in) the
	 * given byte and returns the result.
	 *
	 * @param datum the given byte
	 * @return      the result of encrypting or decrypting the given byte
	 */
	public byte cipher(byte datum) throws Exception {
		byte[] data = {datum};
		return cipher(data)[0];
	}

	/**
	 * Generates a secure stream of bytes based on the input seed.
	 * This routine works by combining the input seed with random bytes
	 * generated by a random number generator, and then computing the 
	 * SHA-1 hash of those bytes.
	 */
	private byte[] generateBytes() throws Exception {
		if (digest == null) {
			digest = MessageDigest.getInstance("SHA"); //$NON-NLS-1$
			//also seed random number generator based on password
			long seed = 0;
			for (int i = 0; i < password.length; i++)
				//this function is known to give good hash distribution for character data
				seed = (seed * 37) + password[i];
			random = new Random(seed);
		}
		//add random bytes to digest array
		random.nextBytes(toDigest);

		//overlay password onto digest array
		System.arraycopy(password, 0, toDigest, 0, password.length);

		//compute and return SHA-1 hash of digest array
		return digest.digest(toDigest);
	}

	/**
	 * Returns a stream of cryptographically secure bytes of the given length.
	 * The result is deterministically based on the input seed (password).
	 */
	private byte[] nextRandom(int length) throws Exception {
		byte[] nextRandom = new byte[length];
		int nextRandomOffset = 0;
		while (nextRandomOffset < length) {
			if (byteStream == null || byteStreamOffset >= byteStream.length) {
				byteStream = generateBytes();
				byteStreamOffset = 0;
			}
			nextRandom[nextRandomOffset++] = byteStream[byteStreamOffset++];
		}
		return nextRandom;
	}

	private byte[] transform(byte[] data, int off, int len, int mod) throws Exception {
		byte[] result = nextRandom(len);
		for (int i = 0; i < len; ++i) {
			result[i] = (byte) (data[i + off] + mod * result[i]);
		}
		return result;
	}
}
