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