|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.sync.crypto; |
|
6 |
|
7 import java.security.GeneralSecurityException; |
|
8 import java.security.InvalidKeyException; |
|
9 import java.security.NoSuchAlgorithmException; |
|
10 import java.util.Arrays; |
|
11 |
|
12 import javax.crypto.BadPaddingException; |
|
13 import javax.crypto.Cipher; |
|
14 import javax.crypto.IllegalBlockSizeException; |
|
15 import javax.crypto.Mac; |
|
16 import javax.crypto.NoSuchPaddingException; |
|
17 import javax.crypto.spec.IvParameterSpec; |
|
18 import javax.crypto.spec.SecretKeySpec; |
|
19 |
|
20 import org.mozilla.apache.commons.codec.binary.Base64; |
|
21 |
|
22 /* |
|
23 * All info in these objects should be decoded (i.e. not BaseXX encoded). |
|
24 */ |
|
25 public class CryptoInfo { |
|
26 private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; |
|
27 private static final String KEY_ALGORITHM_SPEC = "AES"; |
|
28 |
|
29 private byte[] message; |
|
30 private byte[] iv; |
|
31 private byte[] hmac; |
|
32 private KeyBundle keys; |
|
33 |
|
34 /** |
|
35 * Return a CryptoInfo with given plaintext encrypted using given keys. |
|
36 */ |
|
37 public static CryptoInfo encrypt(byte[] plaintextBytes, KeyBundle keys) throws CryptoException { |
|
38 CryptoInfo info = new CryptoInfo(plaintextBytes, keys); |
|
39 info.encrypt(); |
|
40 return info; |
|
41 } |
|
42 |
|
43 /** |
|
44 * Return a CryptoInfo with given plaintext encrypted using given keys and initial vector. |
|
45 */ |
|
46 public static CryptoInfo encrypt(byte[] plaintextBytes, byte[] iv, KeyBundle keys) throws CryptoException { |
|
47 CryptoInfo info = new CryptoInfo(plaintextBytes, iv, null, keys); |
|
48 info.encrypt(); |
|
49 return info; |
|
50 } |
|
51 |
|
52 /** |
|
53 * Return a CryptoInfo with given ciphertext decrypted using given keys and initial vector, verifying that given HMAC validates. |
|
54 */ |
|
55 public static CryptoInfo decrypt(byte[] ciphertext, byte[] iv, byte[] hmac, KeyBundle keys) throws CryptoException { |
|
56 CryptoInfo info = new CryptoInfo(ciphertext, iv, hmac, keys); |
|
57 info.decrypt(); |
|
58 return info; |
|
59 } |
|
60 |
|
61 /* |
|
62 * Constructor typically used when encrypting. |
|
63 */ |
|
64 public CryptoInfo(byte[] message, KeyBundle keys) { |
|
65 this.setMessage(message); |
|
66 this.setKeys(keys); |
|
67 } |
|
68 |
|
69 /* |
|
70 * Constructor typically used when decrypting. |
|
71 */ |
|
72 public CryptoInfo(byte[] message, byte[] iv, byte[] hmac, KeyBundle keys) { |
|
73 this.setMessage(message); |
|
74 this.setIV(iv); |
|
75 this.setHMAC(hmac); |
|
76 this.setKeys(keys); |
|
77 } |
|
78 |
|
79 public byte[] getMessage() { |
|
80 return message; |
|
81 } |
|
82 |
|
83 public void setMessage(byte[] message) { |
|
84 this.message = message; |
|
85 } |
|
86 |
|
87 public byte[] getIV() { |
|
88 return iv; |
|
89 } |
|
90 |
|
91 public void setIV(byte[] iv) { |
|
92 this.iv = iv; |
|
93 } |
|
94 |
|
95 public byte[] getHMAC() { |
|
96 return hmac; |
|
97 } |
|
98 |
|
99 public void setHMAC(byte[] hmac) { |
|
100 this.hmac = hmac; |
|
101 } |
|
102 |
|
103 public KeyBundle getKeys() { |
|
104 return keys; |
|
105 } |
|
106 |
|
107 public void setKeys(KeyBundle keys) { |
|
108 this.keys = keys; |
|
109 } |
|
110 |
|
111 /* |
|
112 * Generate HMAC for given cipher text. |
|
113 */ |
|
114 public static byte[] generatedHMACFor(byte[] message, KeyBundle keys) throws NoSuchAlgorithmException, InvalidKeyException { |
|
115 Mac hmacHasher = HKDF.makeHMACHasher(keys.getHMACKey()); |
|
116 return hmacHasher.doFinal(Base64.encodeBase64(message)); |
|
117 } |
|
118 |
|
119 /* |
|
120 * Return true if generated HMAC is the same as the specified HMAC. |
|
121 */ |
|
122 public boolean generatedHMACIsHMAC() throws NoSuchAlgorithmException, InvalidKeyException { |
|
123 byte[] generatedHMAC = generatedHMACFor(getMessage(), getKeys()); |
|
124 byte[] expectedHMAC = getHMAC(); |
|
125 return Arrays.equals(generatedHMAC, expectedHMAC); |
|
126 } |
|
127 |
|
128 /** |
|
129 * Performs functionality common to both encryption and decryption. |
|
130 * |
|
131 * @param cipher |
|
132 * @param inputMessage non-BaseXX-encoded message |
|
133 * @return encrypted/decrypted message |
|
134 * @throws CryptoException |
|
135 */ |
|
136 private static byte[] commonCrypto(Cipher cipher, byte[] inputMessage) |
|
137 throws CryptoException { |
|
138 byte[] outputMessage = null; |
|
139 try { |
|
140 outputMessage = cipher.doFinal(inputMessage); |
|
141 } catch (IllegalBlockSizeException e) { |
|
142 throw new CryptoException(e); |
|
143 } catch (BadPaddingException e) { |
|
144 throw new CryptoException(e); |
|
145 } |
|
146 return outputMessage; |
|
147 } |
|
148 |
|
149 /** |
|
150 * Encrypt a CryptoInfo in-place. |
|
151 * |
|
152 * @throws CryptoException |
|
153 */ |
|
154 public void encrypt() throws CryptoException { |
|
155 |
|
156 Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); |
|
157 try { |
|
158 byte[] encryptionKey = getKeys().getEncryptionKey(); |
|
159 SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); |
|
160 |
|
161 // If no IV is provided, we allow the cipher to provide one. |
|
162 if (getIV() == null || getIV().length == 0) { |
|
163 cipher.init(Cipher.ENCRYPT_MODE, spec); |
|
164 } else { |
|
165 cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(getIV())); |
|
166 } |
|
167 } catch (GeneralSecurityException ex) { |
|
168 throw new CryptoException(ex); |
|
169 } |
|
170 |
|
171 // Encrypt. |
|
172 byte[] encryptedBytes = commonCrypto(cipher, getMessage()); |
|
173 byte[] iv = cipher.getIV(); |
|
174 |
|
175 byte[] hmac; |
|
176 // Generate HMAC. |
|
177 try { |
|
178 hmac = generatedHMACFor(encryptedBytes, keys); |
|
179 } catch (NoSuchAlgorithmException e) { |
|
180 throw new CryptoException(e); |
|
181 } catch (InvalidKeyException e) { |
|
182 throw new CryptoException(e); |
|
183 } |
|
184 |
|
185 // Update in place. keys is already set. |
|
186 this.setHMAC(hmac); |
|
187 this.setIV(iv); |
|
188 this.setMessage(encryptedBytes); |
|
189 } |
|
190 |
|
191 /** |
|
192 * Decrypt a CryptoInfo in-place. |
|
193 * |
|
194 * @throws CryptoException |
|
195 */ |
|
196 public void decrypt() throws CryptoException { |
|
197 |
|
198 // Check HMAC. |
|
199 try { |
|
200 if (!generatedHMACIsHMAC()) { |
|
201 throw new HMACVerificationException(); |
|
202 } |
|
203 } catch (NoSuchAlgorithmException e) { |
|
204 throw new CryptoException(e); |
|
205 } catch (InvalidKeyException e) { |
|
206 throw new CryptoException(e); |
|
207 } |
|
208 |
|
209 Cipher cipher = CryptoInfo.getCipher(TRANSFORMATION); |
|
210 try { |
|
211 byte[] encryptionKey = getKeys().getEncryptionKey(); |
|
212 SecretKeySpec spec = new SecretKeySpec(encryptionKey, KEY_ALGORITHM_SPEC); |
|
213 cipher.init(Cipher.DECRYPT_MODE, spec, new IvParameterSpec(getIV())); |
|
214 } catch (GeneralSecurityException ex) { |
|
215 throw new CryptoException(ex); |
|
216 } |
|
217 byte[] decryptedBytes = commonCrypto(cipher, getMessage()); |
|
218 byte[] iv = cipher.getIV(); |
|
219 |
|
220 // Update in place. keys is already set. |
|
221 this.setHMAC(null); |
|
222 this.setIV(iv); |
|
223 this.setMessage(decryptedBytes); |
|
224 } |
|
225 |
|
226 /** |
|
227 * Helper to get a Cipher object. |
|
228 * |
|
229 * @param transformation The type of Cipher to get. |
|
230 */ |
|
231 private static Cipher getCipher(String transformation) throws CryptoException { |
|
232 try { |
|
233 return Cipher.getInstance(transformation); |
|
234 } catch (NoSuchAlgorithmException e) { |
|
235 throw new CryptoException(e); |
|
236 } catch (NoSuchPaddingException e) { |
|
237 throw new CryptoException(e); |
|
238 } |
|
239 } |
|
240 } |