AES Java Implementation

From Second Life Wiki
Jump to navigation Jump to search

Description

The following is a simple Java example of AES encryption and decryption, compatible with the LSL AES Engine by Haravikk Mistral.

Required Classes

Base64Coder

package lslAESCrypto;

/**
 * A Base64 Encoder/Decoder.
 * <p>
 * This class is used to encode and decode data in Base64 format as described in
 * RFC 1521.
 * </p>
 */
public class Base64Coder {
	/** Mapping table from 6-bits to Base64 characters. */
	private static char[] BITS_TO_BASE64_CHAR =
		           { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
			'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
			'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
			'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
			'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+',
			'/' };

	/** Mapping table from Base64 characters to 6-bits. */
	private static byte[] BASE64_CHAR_TO_BITS =
		          { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
			-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
			52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1,
			0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
			19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29,
			30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
			47, 48, 49, 50, 51, -1, -1, -1, -1, -1, };

	/**
	 * Decodes a byte array from Base64 format. No blanks or line breaks are
	 * allowed within the Base64 encoded data.
	 * 
	 * @param in
	 *            a character array containing the Base64 encoded data.
	 * @return An array containing the decoded data bytes.
	 * @throws IllegalArgumentException
	 *             if the input is not valid Base64 encoded data.
	 */
	public static byte[] decode(final char[] in)
		throws IllegalArgumentException {
		int len = in.length;
		if (len % 4 != 0)
			throw new IllegalArgumentException(
				"Length of Base64 encoded input string is not a multiple of 4.");

		// Ignore trailing equals
		while (len > 0 && in[len - 1] == '=')
			--len;

		final byte[] bytes = new byte[(len * 3) / 4];
		int o = 0;

		for (int i = 0; i < len;) {
			try {
				final char c0 = in[i++];
				final char c1 = in[i++];
				final char c2 = (i < len) ? in[i++] : 'A';
				final char c3 = (i < len) ? in[i++] : 'A';

				if (c0 > 127 || c1 > 127 || c2 > 127 || c3 > 127)
					throw new IllegalArgumentException(
						"Invalid base64 character");

				final byte b0 = Base64Coder.BASE64_CHAR_TO_BITS[c0];
				final byte b1 = Base64Coder.BASE64_CHAR_TO_BITS[c1];
				final byte b2 = Base64Coder.BASE64_CHAR_TO_BITS[c2];
				final byte b3 = Base64Coder.BASE64_CHAR_TO_BITS[c3];

				if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0)
					throw new IllegalArgumentException(
						"Invalid base64 character");

				bytes[o++] = (byte) ((b0 << 2) | (b1 >>> 4));
				if (o < bytes.length) {
					bytes[o++] = (byte) (((b1 & 0xF) << 4) | (b2 >>> 2));
					if (o < bytes.length)
						bytes[o++] = (byte) (((b2 & 0x3) << 6) | b3);
				}
			} catch (final ArrayIndexOutOfBoundsException e) {
				throw new IllegalArgumentException("Invalid base64 character");
			}
		}

		return bytes;
	}

	/**
	 * Decodes a byte array from Base64 format.
	 * 
	 * @param s
	 *            a Base64 String to be decoded.
	 * @return An array containing the decoded data bytes.
	 * @throws IllegalArgumentException
	 *             if the input is not valid Base64 encoded data.
	 */
	public static byte[] decode(final String s) {
		return Base64Coder.decode(s.toCharArray());
	}

	/**
	 * Decodes a string from Base64 format.
	 * 
	 * @param s
	 *            a Base64 String to be decoded.
	 * @return A String containing the decoded data.
	 * @throws IllegalArgumentException
	 *             if the input is not valid Base64 encoded data.
	 */
	public static String decodeString(final String s) {
		return new String(Base64Coder.decode(s));
	}

	/**
	 * Encodes a byte array into Base64 format. No blanks or line breaks are
	 * inserted.
	 * 
	 * @param in
	 *            an array containing the data bytes to be encoded.
	 * @return A character array with the Base64 encoded data.
	 */
	public static char[] encode(final byte[] in) {
		return Base64Coder.encode(in, 0, in.length);
	}

	/**
	 * Encodes a byte array into Base64 format. No blanks or line breaks are
	 * inserted.
	 * 
	 * @param in
	 *            an array containing the data bytes to be encoded.
	 * @param offset
	 *            the offset into the array at which to begin reading.
	 * @param bits
	 *            number of <b>bits</b> to process from <code>in</code>.
	 * @return A character array with the Base64 encoded data.
	 */
	public static char[] encode(
		final byte[] in,
		final int offset,
		final int bits) {
		int length = bits / 8;
		if ((length * 8) < bits) ++length;

		final char[] chars = new char[((length + 2) / 3) * 4];
		final int out = ((length * 4) + 2) / 3;

		int mask = ~(-1 << (8 - (bits % 8))) | ~(-1 << (bits % 8));
		if (mask == 0) mask = 0xFF;

		int o = 0;
		final int end = length + offset;
		for (int i = offset; i < end;) {
			final int b0 = ((i + 1) == end) ? in[i++] & mask : in[i++] & 0xFF;
			final int b1 =
				(i < length) ? (((i + 1) == end) ? in[i++] & mask
					: in[i++] & 0xFF) : 0;
			final int b2 =
				(i < length) ? (((i + 1) == end) ? in[i++] & mask
					: in[i++] & 0xFF) : 0;

			final int i0 = (b0 >>> 2);
			final int i1 = (((b0 & 0x3) << 4) | (b1 >>> 4));
			final int i2 = (((b1 & 0xF) << 2) | (b2 >>> 6));
			final int i3 = b2 & 0x3F;

			chars[o++] = Base64Coder.BITS_TO_BASE64_CHAR[i0];
			chars[o++] = Base64Coder.BITS_TO_BASE64_CHAR[i1];
			chars[o] = (o < out) ? Base64Coder.BITS_TO_BASE64_CHAR[i2] : '=';
			++o;
			chars[o] = (o < out) ? Base64Coder.BITS_TO_BASE64_CHAR[i3] : '=';
			++o;
		}

		return chars;
	}

	/**
	 * Produces a base64 string from the provided byte-array.
	 * 
	 * @param bytes
	 *            the byte-array to read-from.
	 * @return the base64 encoded string produced.
	 */
	public static String encodeString(final byte[] bytes) {
		return Base64Coder.encodeString(bytes, 0, bytes.length);
	}

	/**
	 * Produces a base64 string from the provided byte-array slice.
	 * 
	 * @param bytes
	 *            the byte-array to read-from.
	 * @param offset
	 *            the offset into the array at which to begin reading.
	 * @param bits
	 *            number of <b>bits</b> to process from <code>bytes</code>.
	 * @return the base64 encoded string produced.
	 */
	public static String encodeString(
		final byte[] bytes,
		final int offset,
		final int bits) {
		return new String(Base64Coder.encode(bytes, offset, bits));
	}

	/**
	 * Encodes a string into Base64 format. No blanks or line breaks are
	 * inserted.
	 * 
	 * @param s
	 *            a String to be encoded.
	 * @return A String with the Base64 encoded data.
	 */
	public static String encodeString(final String s) {
		return new String(Base64Coder.encode(s.getBytes()));
	}

	/** Dummy constructor. */
	private Base64Coder() { /* Blocking constructor */}

}

HexCoder

package lslAESCrypto;
/**
 * The following is a simple set of static methods for converting from hex to
 * bytes and vice-versa
 * 
 * @author Haravikk Mistral
 * @date Sep 15, 2008, 3:26:42 PM
 * @version 1.0
 */
public class HexCoder {
	/**
	 * Quick converts bytes to hex-characters
	 * 
	 * @param bytes
	 *            the byte-array to convert
	 * @return the hex-representation
	 */
	public static String bytesToHex(final byte[] bytes) {
		final StringBuffer s = new StringBuffer(bytes.length * 2);
		for (int i = 0; i < bytes.length; ++i) {
			s.append(Character.forDigit((bytes[i] >> 4) & 0xF, 16));
			s.append(Character.forDigit(bytes[i] & 0xF, 16));
		}
		return s.toString();
	}

	/**
	 * Quickly converts hex-characters to bytes
	 * 
	 * @param s
	 *            the hex-string
	 * @return the bytes represented
	 */
	public static byte[] hexToBytes(final String s) {
		final byte[] bytes = new byte[s.length() / 2];
		for (int i = 0; i < bytes.length; ++i)
			bytes[i] = (byte) Integer.parseInt(
				s.substring(2 * i, (2 * i) + 2),
				16);
		return bytes;
	}
}

Class

package lslAESCrypto;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * <p>
 * This is a class designed to process AES messages sent from the LSL
 * implementation of AES which can be found here:<br/>
 * <a
 * href="https://wiki.secondlife.com/wiki/AES_LSL_Implementation">https://wiki
 * .secondlife.com/wiki/AES_LSL_Implementation</a>
 * </p>
 * <p>
 * This Java class will be updated to support the same modes of operation as the
 * LSL implementation. It currently assumes that keys and input-vectors are
 * processed as hex-strings, and that text is received as plain-text, while
 * ciphertext will be handled as base64 strings.
 * </p>
 * 
 * @author Haravikk
 * @date Sep 15, 2008, 4:18:48 PM
 * @version 1.0
 */
public class LSLAESCrypto {
	/** Our currently set block-cipher mode */
	protected LSLAESCryptoMode mode = LSLAESCryptoMode.CBC;
	/** Used to detect when a new {@link Cipher} Is needed. */
	protected boolean modeChanged = false;

	/** Our currently set padding mode */
	protected LSLAESCryptoPad pad = LSLAESCryptoPad.NONE;
	/** Our currently set pad-size */
	protected int padSize = 512;

	/** The currently loaded key */
	protected SecretKeySpec keySpec = null;
	/** The currently loaded input-vector */
	protected IvParameterSpec ivSpec = null;

	/** The currently active cipher */
	protected Cipher cipher = null;

	/** A random class for secure random operations. */
	protected Random random = new SecureRandom();

	/**
	 * Creates an instance of an LSL compatible AES handler.
	 * 
	 * @param mode
	 *            the cipher-block mode of operation
	 * @param pad
	 *            the padding scheme to use
	 * @param padSize
	 *            the block-size to use when padding. Must be a non-zero,
	 *            positive value that is a multiple of 128.
	 * @param hexKey
	 *            the key to start with (represented as hexadecimal string)
	 * @param hexIV
	 *            the input vector to start with (represented as hexadecimal
	 *            string)
	 * @throws NoSuchAlgorithmException
	 *             if the AES algorithm is not supported by the current JVM
	 * @throws NoSuchPaddingException
	 *             if the padding scheme chosen is not supported by the current
	 *             JVM
	 */
	public LSLAESCrypto(
		final LSLAESCryptoMode mode,
			final LSLAESCryptoPad pad,
			final int padSize,
			final String hexKey,
			final String hexIV)
		throws NoSuchAlgorithmException,
			NoSuchPaddingException {
		this.init(mode, pad, padSize, hexKey, hexIV);
	}

	/**
	 * Decrypts a base64 ciphertext into plain-text
	 * 
	 * @param base64ciphertext
	 *            the ciphertext to decrypt
	 * @return the plain-text that was originally encrypted
	 * @throws InvalidKeyException
	 *             if the currently loaded key is not valid
	 * @throws InvalidAlgorithmParameterException
	 *             if the AES algorithm is not supported by the current JVM
	 * @throws IllegalBlockSizeException
	 *             if the ciphertext is somehow unreadable (bad base64
	 *             conversion)
	 * @throws BadPaddingException
	 *             if the chosen mode of operation requires padded data
	 */
	public String decrypt(final String base64ciphertext)
		throws InvalidKeyException,
			InvalidAlgorithmParameterException,
			IllegalBlockSizeException,
			BadPaddingException {
		if (this.modeChanged) try {
			this.createCipher();
		} catch (final Exception e) { /* Do nothing */}

		this.cipher.init(Cipher.DECRYPT_MODE, this.keySpec, this.ivSpec);
		return new String(this.cipher.doFinal(Base64Coder
			.decode(base64ciphertext)));
	}

	/**
	 * Encrypts plain-text into a base64 string
	 * 
	 * @param text
	 *            the plain-text to encrypt
	 * @return the base64 ciphertext produced
	 * @throws IllegalBlockSizeException
	 *             if the plain text is somehow invalid
	 * @throws BadPaddingException
	 *             if the chosen mode of operation requires padded data
	 * @throws InvalidKeyException
	 *             if the currently loaded key is invalid
	 * @throws InvalidAlgorithmParameterException
	 *             if the AES algorithm is not supported by the current JVM
	 */
	public String encrypt(final String text)
		throws IllegalBlockSizeException,
			BadPaddingException,
			InvalidKeyException,
			InvalidAlgorithmParameterException {
		if (this.modeChanged) try {
			this.createCipher();
		} catch (final Exception e) { /* Do nothing */}

		this.cipher.init(Cipher.ENCRYPT_MODE, this.keySpec, this.ivSpec);

		byte[] data = text.getBytes();
		int bits = data.length * 8;

		/* Apply padding */
		LSLAESCryptoPad padding = this.pad;
		if (padding == LSLAESCryptoPad.NONE) {
			if (this.mode == LSLAESCryptoMode.CFB) { return Base64Coder
				.encodeString(this.cipher.doFinal(data), 0, bits); }
			padding = LSLAESCryptoPad.RBT;
		}

		int blockSize = this.padSize;
		if (padding == LSLAESCryptoPad.RBT) blockSize = 128;

		final int blocks = bits / blockSize;
		int extra = bits % blockSize;

		if (padding == LSLAESCryptoPad.RBT) {
			if (extra > 0) {
				/*
				 * This scheme takes the last encrypted block, encrypts it
				 * again, and XORs it with any leftover data, maintaining
				 * data-length. If input is less than a block in size, then the
				 * current input-vector is used.
				 */
				int bytes = extra / 8;
				if ((bytes * 8) < extra) ++bytes;

				// Grab leftover bytes
				final byte[] t = new byte[bytes];
				if (bytes > 0)
					System.arraycopy(data, data.length - bytes, t, 0, bytes);

				// Encrypt all other data.
				byte[] lb;
				if (blocks < 1) {
					// If not enough for a block, double-encrypt IV.
					data = new byte[0];
					lb =
						this.cipher.doFinal(this.cipher.doFinal(this.ivSpec
							.getIV()));
				} else {
					// If there are blocks, then double-encrypt final one.
					data = this.cipher.doFinal(data, 0, data.length - bytes);
					lb = this.cipher.doFinal(data, data.length - 16, 16);
				}

				// XOR lb with t.
				for (int i = 0; i < t.length; ++i)
					t[i] ^= lb[i];

				lb = new byte[data.length + t.length];
				System.arraycopy(data, 0, lb, 0, data.length);
				System.arraycopy(t, 0, lb, data.length, t.length);

				return Base64Coder.encodeString(lb);
			}
			return Base64Coder.encodeString(this.cipher.doFinal(data), 0, bits);
		}

		// Padding schemes that add bytes until block-boundary is reached.
		extra = blockSize - extra;

		if (padding == LSLAESCryptoPad.NULLS_SAFE) {
			++bits;
			final int bytes = bits / 8;
			final int bit = bytes % 8;

			if (bytes < data.length) data[bytes] |= (1 << (8 - bit));
			else {
				final byte[] t = new byte[data.length + 1];
				System.arraycopy(data, 0, t, 0, data.length);
				t[data.length] = (byte) 0x80;
				data = t;
			}

			if ((--extra) < 0) extra += blockSize;
			padding = LSLAESCryptoPad.NULLS;
		}

		int bytes = extra / 8;
		if (bytes <= 0) {
			if (padding == LSLAESCryptoPad.NULLS)
				return Base64Coder.encodeString(
					this.cipher.doFinal(data),
					0,
					bits);

			bytes = blockSize / 8;
			extra += blockSize;
		}

		bits += extra;
		final byte[] t = new byte[data.length + bytes];
		int i = data.length;
		System.arraycopy(data, 0, t, 0, data.length);
		data = t;

		for (; i < data.length; ++i) {
			byte b = 0;
			if ((i >= (data.length - 4)) && (padding != LSLAESCryptoPad.NULLS)) b =
				(byte) bytes;
			else if (padding == LSLAESCryptoPad.RANDOM)
				b = (byte) this.random.nextInt(256);

			data[i] = b;
		}

		return Base64Coder.encodeString(this.cipher.doFinal(data), 0, bits);
	}

	/**
	 * Initialises this AES instance with a mode, pad, key, and input vector in
	 * a single operation
	 * 
	 * @param mode
	 *            the cipher-block mode of operation
	 * @param pad
	 *            the padding scheme to use
	 * @param padSize
	 *            the block-size to use when padding. Must be a non-zero,
	 *            positive value that is a multiple of 128.
	 * @param hexKey
	 *            the key to use as a hexadecimal string
	 * @param hexIV
	 *            the input-vector to use as a hexadecimal string
	 * @throws NoSuchAlgorithmException
	 *             if the AES algorithm is not supported by the current JVM
	 * @throws NoSuchPaddingException
	 *             if the padding method is not supported by the current JVM
	 */
	public void init(
		final LSLAESCryptoMode mode,
		final LSLAESCryptoPad pad,
		final int padSize,
		final String hexKey,
		final String hexIV)
		throws NoSuchAlgorithmException,
			NoSuchPaddingException {
		if ((mode == null) || (pad == null) || (hexKey == null) ||
			(hexIV == null))
			throw new IllegalArgumentException("No arguments may be null");

		this.setMode(mode);
		this.setPad(pad, padSize);
		this.setKey(hexKey);
		this.setInputVector(hexIV);

		this.random.nextInt();

		this.createCipher();
	}

	/**
	 * Sets the input-vector for this engine to use
	 * 
	 * @param hexIV
	 *            a hexadecimal input-vector to use
	 */
	public void setInputVector(final String hexIV) {
		if (hexIV == null)
			throw new IllegalArgumentException("Input-vector may not be null!");

		this.ivSpec = new IvParameterSpec(HexCoder.hexToBytes(hexIV));
	}

	/**
	 * Sets the key for this engine to use
	 * 
	 * @param hexKey
	 *            a hexadecimal key to use
	 */
	public void setKey(final String hexKey) {
		if (hexKey == null)
			throw new IllegalArgumentException("Key may not be null!");

		this.keySpec = new SecretKeySpec(HexCoder.hexToBytes(hexKey), "AES");
	}

	/**
	 * Sets the mode of this implementation
	 * 
	 * @param mode
	 *            the mode to set
	 */
	public void setMode(final LSLAESCryptoMode mode) {
		if (mode == null)
			throw new IllegalArgumentException("Mode may not be null!");

		this.mode = mode;
		this.modeChanged = true;
	}

	/**
	 * Sets the padding scheme of this implementation
	 * 
	 * @param pad
	 *            the padding scheme to use
	 */
	public void setPad(final LSLAESCryptoPad pad) {
		this.setPad(pad, this.padSize);
	}

	/**
	 * Sets the padding scheme of this implementation
	 * 
	 * @param pad
	 *            the padding scheme to use
	 * @param padSize
	 *            the block-size to use when padding. Must be a non-zero,
	 *            positive value that is a multiple of 128.
	 */
	public void setPad(final LSLAESCryptoPad pad, final int padSize) {
		if (pad == null)
			throw new IllegalArgumentException("Pad may not be null!");
		if ((padSize <= 0) || ((padSize % 128) > 0))
			throw new IllegalArgumentException(
				"Pad size may not be less than zero, and must be a multiple of 128");

		this.pad = pad;
		this.padSize = padSize;
	}

	/**
	 * Creates a new cipher instance for processing
	 * 
	 * @throws NoSuchPaddingException
	 *             if the padding scheme set is invalid
	 * @throws NoSuchAlgorithmException
	 *             if AES is not supported by this JVM
	 */
	protected void createCipher()
		throws NoSuchAlgorithmException,
			NoSuchPaddingException {
		this.cipher = Cipher.getInstance("AES/" + this.mode + "/NoPadding");
	}

	/** Defines modes of operation combatible with LSL */
	public enum LSLAESCryptoMode {
		/** Cipher-Block-Chaining mode */
		CBC,
		/** Cipher FeedBack mode */
		CFB;
	}

	/** Defines padding schemes compatible with LSL */
	public enum LSLAESCryptoPad {
		/** Performs no padding, will switch to RBT if mode is CBC. */
		NONE,
		/**
		 * Enables CFB mode temporarily for the final complete block, and
		 * combines with data. This preserves data-length.
		 */
		RBT,
		/**
		 * Adds null-bytes to the end of the data until it is of correct-size.
		 * This is an padding scheme (may result in loss of null-bytes from
		 * original data).
		 */
		NULLS,
		/**
		 * Same as NULLS, except that it first appends a single '1' bit to the
		 * data before padding.
		 */
		NULLS_SAFE,
		/**
		 * Appends null-bytes to the data until one word from block-size, final
		 * word is then populated with bytes describing the number of padding
		 * bytes added.
		 */
		ZEROES,
		/**
		 * Same as ZEROES, except that random-bytes are used in place of
		 * null-bytes.
		 */
		RANDOM;
	}
}

Examples

Encryption

import lslAESCrypto.LSLAESCrypto;
import lslAESCrypto.LSLAESCrypto.LSLAESCryptoMode;
import lslAESCrypto.LSLAESCrypto.LSLAESCryptoPad;

/** */
public class ExampleEncrypt {
	/**
	 * @param args
	 * @throws Exception
	 */
	public static void main(final String[] args) throws Exception {
		final String myKey = "1234567890ABCDEF0123456789ABCDEF";
		final String myIV = "89ABCDEF0123456789ABCDEF01234567";
		final String myMsg = "Hello world! I am a lovely message waiting to be encrypted!";

		final LSLAESCrypto aes = new LSLAESCrypto(
			LSLAESCryptoMode.CFB,
			LSLAESCryptoPad.NoPadding,
			myKey,
			myIV);
		System.out.println(aes.encrypt(myMsg));
	}
}

Decryption

import lslAESCrypto.LSLAESCrypto;
import lslAESCrypto.LSLAESCrypto.LSLAESCryptoMode;
import lslAESCrypto.LSLAESCrypto.LSLAESCryptoPad;

/** */
public class ExampleDecrypt {
	/**
	 * @param args
	 * @throws Exception
	 */
	public static void main(final String[] args) throws Exception {
		final String myKey = "1234567890ABCDEF0123456789ABCDEF";
		final String myIV = "89ABCDEF0123456789ABCDEF01234567";
		final String myMsg = "Mdn6jGTwRPMOKTYTTdDKGm9KScz26LIz96KVOGAeMw3hpwByPfa07PDRHxRW4TIh5dmu5LlhKpTQChi=";

		final LSLAESCrypto aes = new LSLAESCrypto(
			LSLAESCryptoMode.CFB,
			LSLAESCryptoPad.NoPadding,
			myKey,
			myIV);
		System.out.println(aes.decrypt(myMsg));
	}
}