1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/sync/crypto/CryptoInfo.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,240 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.sync.crypto; 1.9 + 1.10 +import java.security.GeneralSecurityException; 1.11 +import java.security.InvalidKeyException; 1.12 +import java.security.NoSuchAlgorithmException; 1.13 +import java.util.Arrays; 1.14 + 1.15 +import javax.crypto.BadPaddingException; 1.16 +import javax.crypto.Cipher; 1.17 +import javax.crypto.IllegalBlockSizeException; 1.18 +import javax.crypto.Mac; 1.19 +import javax.crypto.NoSuchPaddingException; 1.20 +import javax.crypto.spec.IvParameterSpec; 1.21 +import javax.crypto.spec.SecretKeySpec; 1.22 + 1.23 +import org.mozilla.apache.commons.codec.binary.Base64; 1.24 + 1.25 +/* 1.26 + * All info in these objects should be decoded (i.e. not BaseXX encoded). 1.27 + */ 1.28 +public class CryptoInfo { 1.29 + private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; 1.30 + private static final String KEY_ALGORITHM_SPEC = "AES"; 1.31 + 1.32 + private byte[] message; 1.33 + private byte[] iv; 1.34 + private byte[] hmac; 1.35 + private KeyBundle keys; 1.36 + 1.37 + /** 1.38 + * Return a CryptoInfo with given plaintext encrypted using given keys. 1.39 + */ 1.40 + public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException { 1.41 + CryptoInfo info = new CryptoInfo(plaintextBytes, keys); 1.42 + info.encrypt(); 1.43 + return info; 1.44 + } 1.45 + 1.46 + /** 1.47 + * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector. 1.48 + */ 1.49 + public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException { 1.50 + CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys); 1.51 + info.encrypt(); 1.52 + return info; 1.53 + } 1.54 + 1.55 + /** 1.56 + * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates. 1.57 + */ 1.58 + public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException { 1.59 + CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys); 1.60 + info.decrypt(); 1.61 + return info; 1.62 + } 1.63 + 1.64 + /* 1.65 + * Constructor typically used when encrypting. 1.66 + */ 1.67 + public CryptoInfo(byte[] message, KeyBundle keys) { 1.68 + this.setMessage(message); 1.69 + this.setKeys(keys); 1.70 + } 1.71 + 1.72 + /* 1.73 + * Constructor typically used when decrypting. 1.74 + */ 1.75 + public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) { 1.76 + this.setMessage(message); 1.77 + this.setIV(iv); 1.78 + this.setHMAC(hmac); 1.79 + this.setKeys(keys); 1.80 + } 1.81 + 1.82 + public byte[] getMessage() { 1.83 + return message; 1.84 + } 1.85 + 1.86 + public void setMessage(byte[] message) { 1.87 + this.message = message; 1.88 + } 1.89 + 1.90 + public byte[] getIV() { 1.91 + return iv; 1.92 + } 1.93 + 1.94 + public void setIV(byte[] iv) { 1.95 + this.iv = iv; 1.96 + } 1.97 + 1.98 + public byte[] getHMAC() { 1.99 + return hmac; 1.100 + } 1.101 + 1.102 + public void setHMAC(byte[] hmac) { 1.103 + this.hmac = hmac; 1.104 + } 1.105 + 1.106 + public KeyBundle getKeys() { 1.107 + return keys; 1.108 + } 1.109 + 1.110 + public void setKeys(KeyBundle keys) { 1.111 + this.keys = keys; 1.112 + } 1.113 + 1.114 + /* 1.115 + * Generate HMAC for given cipher text. 1.116 + */ 1.117 + public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException { 1.118 + Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey()); 1.119 + return hmacHasher.doFinal(Base64.encodeBase64(message)); 1.120 + } 1.121 + 1.122 + /* 1.123 + * Return true if generated HMAC is the same as the specified HMAC. 1.124 + */ 1.125 + public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException { 1.126 + byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys()); 1.127 + byte[] expectedHMAC = getHMAC(); 1.128 + return Arrays.equals(generatedHMAC, expectedHMAC); 1.129 + } 1.130 + 1.131 + /** 1.132 + * Performs functionality common to both encryption and decryption. 1.133 + * 1.134 + * @param cipher 1.135 + * @param inputMessage non-BaseXX-encoded message 1.136 + * @return encrypted/decrypted message 1.137 + * @throws CryptoException 1.138 + */ 1.139 + private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage) 1.140 + throws CryptoException { 1.141 + byte[] outputMessage = null; 1.142 + try { 1.143 + outputMessage = cipher.doFinal(inputMessage); 1.144 + } catch (IllegalBlockSizeException e) { 1.145 + throw new CryptoException(e); 1.146 + } catch (BadPaddingException e) { 1.147 + throw new CryptoException(e); 1.148 + } 1.149 + return outputMessage; 1.150 + } 1.151 + 1.152 + /** 1.153 + * Encrypt a CryptoInfo in-place. 1.154 + * 1.155 + * @throws CryptoException 1.156 + */ 1.157 + public void encrypt() throws CryptoException { 1.158 + 1.159 + Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); 1.160 + try { 1.161 + byte[] encryptionKey = getKeys().getEncryptionKey(); 1.162 + SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); 1.163 + 1.164 + // If no IV is provided, we allow the cipher to provide one. 1.165 + if (getIV() == null || getIV().length == 0) { 1.166 + cipher.init(Cipher.ENCRYPT_MODE, spec); 1.167 + } else { 1.168 + cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV())); 1.169 + } 1.170 + } catch (GeneralSecurityException ex) { 1.171 + throw new CryptoException(ex); 1.172 + } 1.173 + 1.174 + // Encrypt. 1.175 + byte[] encryptedBytes = commonCrypto(cipher, getMessage()); 1.176 + byte[] iv = cipher.getIV(); 1.177 + 1.178 + byte[] hmac; 1.179 + // Generate HMAC. 1.180 + try { 1.181 + hmac = generatedHMACFor(encryptedBytes, keys); 1.182 + } catch (NoSuchAlgorithmException e) { 1.183 + throw new CryptoException(e); 1.184 + } catch (InvalidKeyException e) { 1.185 + throw new CryptoException(e); 1.186 + } 1.187 + 1.188 + // Update in place. keys is already set. 1.189 + this.setHMAC(hmac); 1.190 + this.setIV(iv); 1.191 + this.setMessage(encryptedBytes); 1.192 + } 1.193 + 1.194 + /** 1.195 + * Decrypt a CryptoInfo in-place. 1.196 + * 1.197 + * @throws CryptoException 1.198 + */ 1.199 + public void decrypt() throws CryptoException { 1.200 + 1.201 + // Check HMAC. 1.202 + try { 1.203 + if (!generatedHMACIsHMAC()) { 1.204 + throw new HMACVerificationException(); 1.205 + } 1.206 + } catch (NoSuchAlgorithmException e) { 1.207 + throw new CryptoException(e); 1.208 + } catch (InvalidKeyException e) { 1.209 + throw new CryptoException(e); 1.210 + } 1.211 + 1.212 + Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); 1.213 + try { 1.214 + byte[] encryptionKey = getKeys().getEncryptionKey(); 1.215 + SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); 1.216 + cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV())); 1.217 + } catch (GeneralSecurityException ex) { 1.218 + throw new CryptoException(ex); 1.219 + } 1.220 + byte[] decryptedBytes = commonCrypto(cipher, getMessage()); 1.221 + byte[] iv = cipher.getIV(); 1.222 + 1.223 + // Update in place. keys is already set. 1.224 + this.setHMAC(null); 1.225 + this.setIV(iv); 1.226 + this.setMessage(decryptedBytes); 1.227 + } 1.228 + 1.229 + /** 1.230 + * Helper to get a Cipher object. 1.231 + * 1.232 + * @param transformation The type of Cipher to get. 1.233 + */ 1.234 + private static Cipher getCipher(String transformation) throws CryptoException { 1.235 + try { 1.236 + return Cipher.getInstance(transformation); 1.237 + } catch (NoSuchAlgorithmException e) { 1.238 + throw new CryptoException(e); 1.239 + } catch (NoSuchPaddingException e) { 1.240 + throw new CryptoException(e); 1.241 + } 1.242 + } 1.243 +}