michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync.crypto; michael@0: michael@0: import java.security.GeneralSecurityException; michael@0: import java.security.InvalidKeyException; michael@0: import java.security.NoSuchAlgorithmException; michael@0: import java.util.Arrays; michael@0: michael@0: import javax.crypto.BadPaddingException; michael@0: import javax.crypto.Cipher; michael@0: import javax.crypto.IllegalBlockSizeException; michael@0: import javax.crypto.Mac; michael@0: import javax.crypto.NoSuchPaddingException; michael@0: import javax.crypto.spec.IvParameterSpec; michael@0: import javax.crypto.spec.SecretKeySpec; michael@0: michael@0: import org.mozilla.apache.commons.codec.binary.Base64; michael@0: michael@0: /* michael@0: * All info in these objects should be decoded (i.e. not BaseXX encoded). michael@0: */ michael@0: public class CryptoInfo { michael@0: private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; michael@0: private static final String KEY_ALGORITHM_SPEC = "AES"; michael@0: michael@0: private byte[] message; michael@0: private byte[] iv; michael@0: private byte[] hmac; michael@0: private KeyBundle keys; michael@0: michael@0: /** michael@0: * Return a CryptoInfo with given plaintext encrypted using given keys. michael@0: */ michael@0: public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException { michael@0: CryptoInfo info = new CryptoInfo(plaintextBytes, keys); michael@0: info.encrypt(); michael@0: return info; michael@0: } michael@0: michael@0: /** michael@0: * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector. michael@0: */ michael@0: public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException { michael@0: CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys); michael@0: info.encrypt(); michael@0: return info; michael@0: } michael@0: michael@0: /** michael@0: * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates. michael@0: */ michael@0: public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException { michael@0: CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys); michael@0: info.decrypt(); michael@0: return info; michael@0: } michael@0: michael@0: /* michael@0: * Constructor typically used when encrypting. michael@0: */ michael@0: public CryptoInfo(byte[] message, KeyBundle keys) { michael@0: this.setMessage(message); michael@0: this.setKeys(keys); michael@0: } michael@0: michael@0: /* michael@0: * Constructor typically used when decrypting. michael@0: */ michael@0: public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) { michael@0: this.setMessage(message); michael@0: this.setIV(iv); michael@0: this.setHMAC(hmac); michael@0: this.setKeys(keys); michael@0: } michael@0: michael@0: public byte[] getMessage() { michael@0: return message; michael@0: } michael@0: michael@0: public void setMessage(byte[] message) { michael@0: this.message = message; michael@0: } michael@0: michael@0: public byte[] getIV() { michael@0: return iv; michael@0: } michael@0: michael@0: public void setIV(byte[] iv) { michael@0: this.iv = iv; michael@0: } michael@0: michael@0: public byte[] getHMAC() { michael@0: return hmac; michael@0: } michael@0: michael@0: public void setHMAC(byte[] hmac) { michael@0: this.hmac = hmac; michael@0: } michael@0: michael@0: public KeyBundle getKeys() { michael@0: return keys; michael@0: } michael@0: michael@0: public void setKeys(KeyBundle keys) { michael@0: this.keys = keys; michael@0: } michael@0: michael@0: /* michael@0: * Generate HMAC for given cipher text. michael@0: */ michael@0: public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException { michael@0: Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey()); michael@0: return hmacHasher.doFinal(Base64.encodeBase64(message)); michael@0: } michael@0: michael@0: /* michael@0: * Return true if generated HMAC is the same as the specified HMAC. michael@0: */ michael@0: public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException { michael@0: byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys()); michael@0: byte[] expectedHMAC = getHMAC(); michael@0: return Arrays.equals(generatedHMAC, expectedHMAC); michael@0: } michael@0: michael@0: /** michael@0: * Performs functionality common to both encryption and decryption. michael@0: * michael@0: * @param cipher michael@0: * @param inputMessage non-BaseXX-encoded message michael@0: * @return encrypted/decrypted message michael@0: * @throws CryptoException michael@0: */ michael@0: private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage) michael@0: throws CryptoException { michael@0: byte[] outputMessage = null; michael@0: try { michael@0: outputMessage = cipher.doFinal(inputMessage); michael@0: } catch (IllegalBlockSizeException e) { michael@0: throw new CryptoException(e); michael@0: } catch (BadPaddingException e) { michael@0: throw new CryptoException(e); michael@0: } michael@0: return outputMessage; michael@0: } michael@0: michael@0: /** michael@0: * Encrypt a CryptoInfo in-place. michael@0: * michael@0: * @throws CryptoException michael@0: */ michael@0: public void encrypt() throws CryptoException { michael@0: michael@0: Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); michael@0: try { michael@0: byte[] encryptionKey = getKeys().getEncryptionKey(); michael@0: SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); michael@0: michael@0: // If no IV is provided, we allow the cipher to provide one. michael@0: if (getIV() == null || getIV().length == 0) { michael@0: cipher.init(Cipher.ENCRYPT_MODE, spec); michael@0: } else { michael@0: cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV())); michael@0: } michael@0: } catch (GeneralSecurityException ex) { michael@0: throw new CryptoException(ex); michael@0: } michael@0: michael@0: // Encrypt. michael@0: byte[] encryptedBytes = commonCrypto(cipher, getMessage()); michael@0: byte[] iv = cipher.getIV(); michael@0: michael@0: byte[] hmac; michael@0: // Generate HMAC. michael@0: try { michael@0: hmac = generatedHMACFor(encryptedBytes, keys); michael@0: } catch (NoSuchAlgorithmException e) { michael@0: throw new CryptoException(e); michael@0: } catch (InvalidKeyException e) { michael@0: throw new CryptoException(e); michael@0: } michael@0: michael@0: // Update in place. keys is already set. michael@0: this.setHMAC(hmac); michael@0: this.setIV(iv); michael@0: this.setMessage(encryptedBytes); michael@0: } michael@0: michael@0: /** michael@0: * Decrypt a CryptoInfo in-place. michael@0: * michael@0: * @throws CryptoException michael@0: */ michael@0: public void decrypt() throws CryptoException { michael@0: michael@0: // Check HMAC. michael@0: try { michael@0: if (!generatedHMACIsHMAC()) { michael@0: throw new HMACVerificationException(); michael@0: } michael@0: } catch (NoSuchAlgorithmException e) { michael@0: throw new CryptoException(e); michael@0: } catch (InvalidKeyException e) { michael@0: throw new CryptoException(e); michael@0: } michael@0: michael@0: Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); michael@0: try { michael@0: byte[] encryptionKey = getKeys().getEncryptionKey(); michael@0: SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); michael@0: cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV())); michael@0: } catch (GeneralSecurityException ex) { michael@0: throw new CryptoException(ex); michael@0: } michael@0: byte[] decryptedBytes = commonCrypto(cipher, getMessage()); michael@0: byte[] iv = cipher.getIV(); michael@0: michael@0: // Update in place. keys is already set. michael@0: this.setHMAC(null); michael@0: this.setIV(iv); michael@0: this.setMessage(decryptedBytes); michael@0: } michael@0: michael@0: /** michael@0: * Helper to get a Cipher object. michael@0: * michael@0: * @param transformation The type of Cipher to get. michael@0: */ michael@0: private static Cipher getCipher(String transformation) throws CryptoException { michael@0: try { michael@0: return Cipher.getInstance(transformation); michael@0: } catch (NoSuchAlgorithmException e) { michael@0: throw new CryptoException(e); michael@0: } catch (NoSuchPaddingException e) { michael@0: throw new CryptoException(e); michael@0: } michael@0: } michael@0: }