Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | package org.mozilla.gecko.sync.crypto; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.UnsupportedEncodingException; |
michael@0 | 8 | import java.security.InvalidKeyException; |
michael@0 | 9 | import java.security.NoSuchAlgorithmException; |
michael@0 | 10 | import java.util.Arrays; |
michael@0 | 11 | |
michael@0 | 12 | import javax.crypto.KeyGenerator; |
michael@0 | 13 | import javax.crypto.Mac; |
michael@0 | 14 | |
michael@0 | 15 | import org.mozilla.apache.commons.codec.binary.Base64; |
michael@0 | 16 | import org.mozilla.gecko.sync.Utils; |
michael@0 | 17 | |
michael@0 | 18 | public class KeyBundle { |
michael@0 | 19 | private static final String KEY_ALGORITHM_SPEC = "AES"; |
michael@0 | 20 | private static final int KEY_SIZE = 256; |
michael@0 | 21 | |
michael@0 | 22 | private byte[] encryptionKey; |
michael@0 | 23 | private byte[] hmacKey; |
michael@0 | 24 | |
michael@0 | 25 | // These are the same for every sync key bundle. |
michael@0 | 26 | private static final byte[] EMPTY_BYTES = {}; |
michael@0 | 27 | private static final byte[] ENCR_INPUT_BYTES = {1}; |
michael@0 | 28 | private static final byte[] HMAC_INPUT_BYTES = {2}; |
michael@0 | 29 | |
michael@0 | 30 | /* |
michael@0 | 31 | * Mozilla's use of HKDF for getting keys from the Sync Key string. |
michael@0 | 32 | * |
michael@0 | 33 | * We do exactly 2 HKDF iterations and make the first iteration the |
michael@0 | 34 | * encryption key and the second iteration the HMAC key. |
michael@0 | 35 | * |
michael@0 | 36 | */ |
michael@0 | 37 | public KeyBundle(String username, String base32SyncKey) throws CryptoException { |
michael@0 | 38 | if (base32SyncKey == null) { |
michael@0 | 39 | throw new IllegalArgumentException("No sync key provided."); |
michael@0 | 40 | } |
michael@0 | 41 | if (username == null || username.equals("")) { |
michael@0 | 42 | throw new IllegalArgumentException("No username provided."); |
michael@0 | 43 | } |
michael@0 | 44 | // Hash appropriately. |
michael@0 | 45 | try { |
michael@0 | 46 | username = Utils.usernameFromAccount(username); |
michael@0 | 47 | } catch (NoSuchAlgorithmException e) { |
michael@0 | 48 | throw new IllegalArgumentException("Invalid username."); |
michael@0 | 49 | } catch (UnsupportedEncodingException e) { |
michael@0 | 50 | throw new IllegalArgumentException("Invalid username."); |
michael@0 | 51 | } |
michael@0 | 52 | |
michael@0 | 53 | byte[] syncKey = Utils.decodeFriendlyBase32(base32SyncKey); |
michael@0 | 54 | byte[] user = username.getBytes(); |
michael@0 | 55 | |
michael@0 | 56 | Mac hmacHasher; |
michael@0 | 57 | try { |
michael@0 | 58 | hmacHasher = HKDF.makeHMACHasher(syncKey); |
michael@0 | 59 | } catch (NoSuchAlgorithmException e) { |
michael@0 | 60 | throw new CryptoException(e); |
michael@0 | 61 | } catch (InvalidKeyException e) { |
michael@0 | 62 | throw new CryptoException(e); |
michael@0 | 63 | } |
michael@0 | 64 | assert(hmacHasher != null); // If makeHMACHasher doesn't throw, then hmacHasher is non-null. |
michael@0 | 65 | |
michael@0 | 66 | byte[] encrBytes = Utils.concatAll(EMPTY_BYTES, HKDF.HMAC_INPUT, user, ENCR_INPUT_BYTES); |
michael@0 | 67 | byte[] encrKey = HKDF.digestBytes(encrBytes, hmacHasher); |
michael@0 | 68 | byte[] hmacBytes = Utils.concatAll(encrKey, HKDF.HMAC_INPUT, user, HMAC_INPUT_BYTES); |
michael@0 | 69 | |
michael@0 | 70 | this.hmacKey = HKDF.digestBytes(hmacBytes, hmacHasher); |
michael@0 | 71 | this.encryptionKey = encrKey; |
michael@0 | 72 | } |
michael@0 | 73 | |
michael@0 | 74 | public KeyBundle(byte[] encryptionKey, byte[] hmacKey) { |
michael@0 | 75 | this.setEncryptionKey(encryptionKey); |
michael@0 | 76 | this.setHMACKey(hmacKey); |
michael@0 | 77 | } |
michael@0 | 78 | |
michael@0 | 79 | /** |
michael@0 | 80 | * Make a KeyBundle with the specified base64-encoded keys. |
michael@0 | 81 | * |
michael@0 | 82 | * @return A KeyBundle with the specified keys. |
michael@0 | 83 | */ |
michael@0 | 84 | public static KeyBundle fromBase64EncodedKeys(String base64EncryptionKey, String base64HmacKey) throws UnsupportedEncodingException { |
michael@0 | 85 | return new KeyBundle(Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")), |
michael@0 | 86 | Base64.decodeBase64(base64HmacKey.getBytes("UTF-8"))); |
michael@0 | 87 | } |
michael@0 | 88 | |
michael@0 | 89 | /** |
michael@0 | 90 | * Make a KeyBundle with two random 256 bit keys (encryption and HMAC). |
michael@0 | 91 | * |
michael@0 | 92 | * @return A KeyBundle with random keys. |
michael@0 | 93 | */ |
michael@0 | 94 | public static KeyBundle withRandomKeys() throws CryptoException { |
michael@0 | 95 | KeyGenerator keygen; |
michael@0 | 96 | try { |
michael@0 | 97 | keygen = KeyGenerator.getInstance(KEY_ALGORITHM_SPEC); |
michael@0 | 98 | } catch (NoSuchAlgorithmException e) { |
michael@0 | 99 | throw new CryptoException(e); |
michael@0 | 100 | } |
michael@0 | 101 | |
michael@0 | 102 | keygen.init(KEY_SIZE); |
michael@0 | 103 | byte[] encryptionKey = keygen.generateKey().getEncoded(); |
michael@0 | 104 | byte[] hmacKey = keygen.generateKey().getEncoded(); |
michael@0 | 105 | |
michael@0 | 106 | return new KeyBundle(encryptionKey, hmacKey); |
michael@0 | 107 | } |
michael@0 | 108 | |
michael@0 | 109 | public byte[] getEncryptionKey() { |
michael@0 | 110 | return encryptionKey; |
michael@0 | 111 | } |
michael@0 | 112 | |
michael@0 | 113 | public void setEncryptionKey(byte[] encryptionKey) { |
michael@0 | 114 | this.encryptionKey = encryptionKey; |
michael@0 | 115 | } |
michael@0 | 116 | |
michael@0 | 117 | public byte[] getHMACKey() { |
michael@0 | 118 | return hmacKey; |
michael@0 | 119 | } |
michael@0 | 120 | |
michael@0 | 121 | public void setHMACKey(byte[] hmacKey) { |
michael@0 | 122 | this.hmacKey = hmacKey; |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | @Override |
michael@0 | 126 | public boolean equals(Object o) { |
michael@0 | 127 | if (!(o instanceof KeyBundle)) { |
michael@0 | 128 | return false; |
michael@0 | 129 | } |
michael@0 | 130 | KeyBundle other = (KeyBundle) o; |
michael@0 | 131 | return Arrays.equals(other.encryptionKey, this.encryptionKey) && |
michael@0 | 132 | Arrays.equals(other.hmacKey, this.hmacKey); |
michael@0 | 133 | } |
michael@0 | 134 | |
michael@0 | 135 | @Override |
michael@0 | 136 | public int hashCode() { |
michael@0 | 137 | throw new UnsupportedOperationException("No hashCode for KeyBundle."); |
michael@0 | 138 | } |
michael@0 | 139 | } |