/**
* GmailAssistant 1.1 (2008-03-16)
* Copyright 2008 Zach Scrivena
* zachscrivena@gmail.com
* http://gmailassistant.sourceforge.net/
*
* Notifier for multiple Gmail accounts.
*
* TERMS AND CONDITIONS:
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package gmailassistant;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Provide convenient methods for encryption.
*/
class Encryptor
{
/** name of the character set to use for converting between characters and bytes */
private static final String CHARSET_NAME = "UTF-8";
/** random number generator algorithm */
private static final String RNG_ALGORITHM = "SHA1PRNG";
/** message digest algorithm (must be sufficiently long to provide the key and initialization vector) */
private static final String DIGEST_ALGORITHM = "SHA-256";
/** key algorithm (must be compatible with CIPHER_ALGORITHM) */
private static final String KEY_ALGORITHM = "AES";
/** cipher algorithm */
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
/** length of the salt, in bytes */
static final int SALT_LENGTH = 8;
/** iteration count for encryption */
private static final int ITERATION_COUNT = 1024;
/**
* Encrypt the specified cleartext with the given password.
* With the correct salt and password, the decrypt() method reverses the effect of this method.
* This method generates and uses a random 8-byte salt, and the user-specified password
* to create a 16-byte secret key and 16-byte initialization vector.
* The secret key and initialization vector are used in the AES-128 cipher to encrypt
* the given cleartext.
*
* @param salt
* salt (8-byte) that was used in the encryption (to be populated)
* @param password
* password to be used in the encryption
* @param cleartext
* cleartext to be encrypted
* @return
* ciphertext
* @throws Exception
* on any error encountered in encryption
*/
static byte[] encrypt(
final byte[] salt,
final String password,
final byte[] cleartext)
throws Exception
{
/* generate 8-byte salt randomly */
if (salt.length != Encryptor.SALT_LENGTH)
{
throw new IllegalArgumentException("Salt array must be 8 bytes long.");
}
SecureRandom.getInstance(Encryptor.RNG_ALGORITHM).nextBytes(salt);
/* compute key and initialization vector */
final MessageDigest shaDigest = MessageDigest.getInstance(Encryptor.DIGEST_ALGORITHM);
byte[] pw = password.getBytes(Encryptor.CHARSET_NAME);
for (int i = 0; i < Encryptor.ITERATION_COUNT; i++)
{
/* add salt */
final byte[] salted = new byte[pw.length + Encryptor.SALT_LENGTH];
System.arraycopy(pw, 0, salted, 0, pw.length);
System.arraycopy(salt, 0, salted, pw.length, Encryptor.SALT_LENGTH);
Arrays.fill(pw, (byte) 0x00);
/* compute SHA-256 digest */
shaDigest.reset();
pw = shaDigest.digest(salted);
Arrays.fill(salted, (byte) 0x00);
}
/* extract the 16-byte key and initialization vector from the SHA-256 digest */
final byte[] key = new byte[16];
final byte[] iv = new byte[16];
System.arraycopy(pw, 0, key, 0, 16);
System.arraycopy(pw, 16, iv, 0, 16);
Arrays.fill(pw, (byte) 0x00);
/* perform AES-128 encryption */
final Cipher cipher = Cipher.getInstance(Encryptor.CIPHER_ALGORITHM);
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, Encryptor.KEY_ALGORITHM),
new IvParameterSpec(iv));
Arrays.fill(key, (byte) 0x00);
Arrays.fill(iv, (byte) 0x00);
return cipher.doFinal(cleartext);
}
/**
* Decrypt the specified ciphertext with the given salt and password.
* With the correct salt and password, this method reverses the effect of the encrypt() method.
* This method uses the user-specified 8-byte salt, and password
* to recreate the 16-byte secret key and 16-byte initialization vector.
* The secret key and initialization vector are used in the AES-128 cipher to decrypt
* the given ciphertext.
*
* @param salt
* salt (8-byte) to be used in decryption
* @param password
* password to be used in the decryption
* @param ciphertext
* ciphertext to be decrypted
* @return
* cleartext
* @throws Exception
* on any error encountered in decryption
*/
static byte[] decrypt(
final byte[] salt,
final String password,
final byte[] ciphertext)
throws Exception
{
if (salt.length != Encryptor.SALT_LENGTH)
{
throw new IllegalArgumentException("Salt array must be 8 bytes long.");
}
/* compute key and initialization vector */
final MessageDigest shaDigest = MessageDigest.getInstance(Encryptor.DIGEST_ALGORITHM);
byte[] pw = password.getBytes(Encryptor.CHARSET_NAME);
for (int i = 0; i < Encryptor.ITERATION_COUNT; i++)
{
/* add salt */
final byte[] salted = new byte[pw.length + Encryptor.SALT_LENGTH];
System.arraycopy(pw, 0, salted, 0, pw.length);
System.arraycopy(salt, 0, salted, pw.length, Encryptor.SALT_LENGTH);
Arrays.fill(pw, (byte) 0x00);
/* compute SHA-256 digest */
shaDigest.reset();
pw = shaDigest.digest(salted);
Arrays.fill(salted, (byte) 0x00);
}
/* extract the 16-byte key and initialization vector from the SHA-256 digest */
final byte[] key = new byte[16];
final byte[] iv = new byte[16];
System.arraycopy(pw, 0, key, 0, 16);
System.arraycopy(pw, 16, iv, 0, 16);
Arrays.fill(pw, (byte) 0x00);
/* perform AES-128 decryption */
final Cipher cipher = Cipher.getInstance(Encryptor.CIPHER_ALGORITHM);
cipher.init(
Cipher.DECRYPT_MODE,
new SecretKeySpec(key, Encryptor.KEY_ALGORITHM),
new IvParameterSpec(iv));
Arrays.fill(key, (byte) 0x00);
Arrays.fill(iv, (byte) 0x00);
return cipher.doFinal(ciphertext);
}
/**
* Convert a list of strings to a newly created byte array.
* Note that this method does not explicitly delimit elements in the list of strings;
* this has to be done externally.
*
* @param list
* list of strings
* @return
* byte array
*/
static byte[] stringListToByteArray(
final List<String> listStrings)
throws UnsupportedEncodingException
{
final List<byte[]> listBytes = new ArrayList<byte[]>();
int length = 0;
for (String s : listStrings)
{
final byte[] b = s.getBytes(Encryptor.CHARSET_NAME);
listBytes.add(b);
length += b.length;
}
final byte[] byteArray = new byte[length];
int pos = 0;
for (byte[] b : listBytes)
{
System.arraycopy(b, 0, byteArray, pos, b.length);
pos += b.length;
Arrays.fill(b, (byte) 0x00); /* zero out temporary byte buffers */
}
return byteArray;
}
/**
* Convert a byte array to an array of strings.
* The specified delimiter string is used to separate elements in the array of strings,
* and is excluded from the array.
*
* @param byteArray
* byte array
* @param delimiter
* delimiter string
* @return
* array of strings
*/
static String[] byteArrayToStringArray(
final byte[] byteArray,
final String delimiter)
throws UnsupportedEncodingException
{
final String s = new String(byteArray, Encryptor.CHARSET_NAME);
return s.split(Pattern.quote(delimiter));
}
}