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; |
michael@0 | 6 | |
michael@0 | 7 | import java.io.IOException; |
michael@0 | 8 | import java.io.UnsupportedEncodingException; |
michael@0 | 9 | import java.util.HashMap; |
michael@0 | 10 | import java.util.HashSet; |
michael@0 | 11 | import java.util.Map.Entry; |
michael@0 | 12 | import java.util.Set; |
michael@0 | 13 | |
michael@0 | 14 | import org.json.simple.JSONArray; |
michael@0 | 15 | import org.json.simple.parser.ParseException; |
michael@0 | 16 | import org.mozilla.apache.commons.codec.binary.Base64; |
michael@0 | 17 | import org.mozilla.gecko.sync.crypto.CryptoException; |
michael@0 | 18 | import org.mozilla.gecko.sync.crypto.KeyBundle; |
michael@0 | 19 | |
michael@0 | 20 | public class CollectionKeys { |
michael@0 | 21 | private KeyBundle defaultKeyBundle = null; |
michael@0 | 22 | private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>(); |
michael@0 | 23 | |
michael@0 | 24 | /** |
michael@0 | 25 | * Randomly generate a basic CollectionKeys object. |
michael@0 | 26 | * @throws CryptoException |
michael@0 | 27 | */ |
michael@0 | 28 | public static CollectionKeys generateCollectionKeys() throws CryptoException { |
michael@0 | 29 | CollectionKeys ck = new CollectionKeys(); |
michael@0 | 30 | ck.clear(); |
michael@0 | 31 | ck.defaultKeyBundle = KeyBundle.withRandomKeys(); |
michael@0 | 32 | // TODO: eventually we would like to keep per-collection keys, just generate |
michael@0 | 33 | // new ones as appropriate. |
michael@0 | 34 | return ck; |
michael@0 | 35 | } |
michael@0 | 36 | |
michael@0 | 37 | public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException { |
michael@0 | 38 | if (this.defaultKeyBundle == null) { |
michael@0 | 39 | throw new NoCollectionKeysSetException(); |
michael@0 | 40 | } |
michael@0 | 41 | return this.defaultKeyBundle; |
michael@0 | 42 | } |
michael@0 | 43 | |
michael@0 | 44 | public boolean keyBundleForCollectionIsNotDefault(String collection) { |
michael@0 | 45 | return collectionKeyBundles.containsKey(collection); |
michael@0 | 46 | } |
michael@0 | 47 | |
michael@0 | 48 | public KeyBundle keyBundleForCollection(String collection) |
michael@0 | 49 | throws NoCollectionKeysSetException { |
michael@0 | 50 | if (this.defaultKeyBundle == null) { |
michael@0 | 51 | throw new NoCollectionKeysSetException(); |
michael@0 | 52 | } |
michael@0 | 53 | if (keyBundleForCollectionIsNotDefault(collection)) { |
michael@0 | 54 | return collectionKeyBundles.get(collection); |
michael@0 | 55 | } |
michael@0 | 56 | return this.defaultKeyBundle; |
michael@0 | 57 | } |
michael@0 | 58 | |
michael@0 | 59 | /** |
michael@0 | 60 | * Take a pair of values in a JSON array, handing them off to KeyBundle to |
michael@0 | 61 | * produce a usable keypair. |
michael@0 | 62 | */ |
michael@0 | 63 | private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException { |
michael@0 | 64 | String encKeyStr = (String) array.get(0); |
michael@0 | 65 | String hmacKeyStr = (String) array.get(1); |
michael@0 | 66 | return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr); |
michael@0 | 67 | } |
michael@0 | 68 | |
michael@0 | 69 | @SuppressWarnings("unchecked") |
michael@0 | 70 | private static JSONArray keyBundleToArray(KeyBundle bundle) { |
michael@0 | 71 | // Generate JSON. |
michael@0 | 72 | JSONArray keysArray = new JSONArray(); |
michael@0 | 73 | keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey()))); |
michael@0 | 74 | keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey()))); |
michael@0 | 75 | return keysArray; |
michael@0 | 76 | } |
michael@0 | 77 | |
michael@0 | 78 | private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException { |
michael@0 | 79 | ExtendedJSONObject json = new ExtendedJSONObject(); |
michael@0 | 80 | json.put("id", "keys"); |
michael@0 | 81 | json.put("collection", "crypto"); |
michael@0 | 82 | json.put("default", keyBundleToArray(this.defaultKeyBundle())); |
michael@0 | 83 | ExtendedJSONObject colls = new ExtendedJSONObject(); |
michael@0 | 84 | for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) { |
michael@0 | 85 | colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue())); |
michael@0 | 86 | } |
michael@0 | 87 | json.put("collections", colls); |
michael@0 | 88 | return json; |
michael@0 | 89 | } |
michael@0 | 90 | |
michael@0 | 91 | public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException { |
michael@0 | 92 | ExtendedJSONObject payload = this.asRecordContents(); |
michael@0 | 93 | CryptoRecord record = new CryptoRecord(payload); |
michael@0 | 94 | record.collection = "crypto"; |
michael@0 | 95 | record.guid = "keys"; |
michael@0 | 96 | record.deleted = false; |
michael@0 | 97 | return record; |
michael@0 | 98 | } |
michael@0 | 99 | |
michael@0 | 100 | /** |
michael@0 | 101 | * Set my key bundle and collection keys with the given key bundle and data |
michael@0 | 102 | * (possibly decrypted) from the given record. |
michael@0 | 103 | * |
michael@0 | 104 | * @param keys |
michael@0 | 105 | * A "crypto/keys" <code>CryptoRecord</code>, encrypted with |
michael@0 | 106 | * <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null. |
michael@0 | 107 | * @param syncKeyBundle |
michael@0 | 108 | * If non-null, the sync key bundle to decrypt <code>keys</code> with. |
michael@0 | 109 | */ |
michael@0 | 110 | public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle) |
michael@0 | 111 | throws CryptoException, IOException, ParseException, NonObjectJSONException { |
michael@0 | 112 | if (keys == null) { |
michael@0 | 113 | throw new IllegalArgumentException("cannot set key pairs from null record"); |
michael@0 | 114 | } |
michael@0 | 115 | if (syncKeyBundle != null) { |
michael@0 | 116 | keys.keyBundle = syncKeyBundle; |
michael@0 | 117 | keys.decrypt(); |
michael@0 | 118 | } |
michael@0 | 119 | ExtendedJSONObject cleartext = keys.payload; |
michael@0 | 120 | KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default")); |
michael@0 | 121 | |
michael@0 | 122 | ExtendedJSONObject collections = cleartext.getObject("collections"); |
michael@0 | 123 | HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>(); |
michael@0 | 124 | for (Entry<String, Object> pair : collections.entrySet()) { |
michael@0 | 125 | KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue()); |
michael@0 | 126 | collectionKeys.put(pair.getKey(), bundle); |
michael@0 | 127 | } |
michael@0 | 128 | |
michael@0 | 129 | this.collectionKeyBundles.clear(); |
michael@0 | 130 | this.collectionKeyBundles.putAll(collectionKeys); |
michael@0 | 131 | this.defaultKeyBundle = defaultKey; |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | public void setKeyBundleForCollection(String collection, KeyBundle keys) { |
michael@0 | 135 | this.collectionKeyBundles.put(collection, keys); |
michael@0 | 136 | } |
michael@0 | 137 | |
michael@0 | 138 | public void setDefaultKeyBundle(KeyBundle keys) { |
michael@0 | 139 | this.defaultKeyBundle = keys; |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | public void clear() { |
michael@0 | 143 | this.defaultKeyBundle = null; |
michael@0 | 144 | this.collectionKeyBundles.clear(); |
michael@0 | 145 | } |
michael@0 | 146 | |
michael@0 | 147 | /** |
michael@0 | 148 | * Return set of collections where key is either missing from one collection |
michael@0 | 149 | * or not the same in both collections. |
michael@0 | 150 | * <p> |
michael@0 | 151 | * Does not check for different default keys. |
michael@0 | 152 | */ |
michael@0 | 153 | public static Set<String> differences(CollectionKeys a, CollectionKeys b) { |
michael@0 | 154 | Set<String> differences = new HashSet<String>(); |
michael@0 | 155 | Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet()); |
michael@0 | 156 | collections.addAll(b.collectionKeyBundles.keySet()); |
michael@0 | 157 | |
michael@0 | 158 | // Iterate through one collection, collecting missing and differences. |
michael@0 | 159 | for (String collection : collections) { |
michael@0 | 160 | KeyBundle keyA; |
michael@0 | 161 | KeyBundle keyB; |
michael@0 | 162 | try { |
michael@0 | 163 | keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate. |
michael@0 | 164 | keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate. |
michael@0 | 165 | } catch (NoCollectionKeysSetException e) { |
michael@0 | 166 | differences.add(collection); |
michael@0 | 167 | continue; |
michael@0 | 168 | } |
michael@0 | 169 | // keyA and keyB are not null at this point. |
michael@0 | 170 | if (!keyA.equals(keyB)) { |
michael@0 | 171 | differences.add(collection); |
michael@0 | 172 | } |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | return differences; |
michael@0 | 176 | } |
michael@0 | 177 | |
michael@0 | 178 | @Override |
michael@0 | 179 | public boolean equals(Object o) { |
michael@0 | 180 | if (!(o instanceof CollectionKeys)) { |
michael@0 | 181 | return false; |
michael@0 | 182 | } |
michael@0 | 183 | CollectionKeys other = (CollectionKeys) o; |
michael@0 | 184 | try { |
michael@0 | 185 | // It would be nice to use map equality here, but there can be map entries |
michael@0 | 186 | // where the key is the default key that should compare equal to a missing |
michael@0 | 187 | // map entry. Therefore, we always compute the set of differences. |
michael@0 | 188 | return defaultKeyBundle().equals(other.defaultKeyBundle()) && |
michael@0 | 189 | CollectionKeys.differences(this, other).isEmpty(); |
michael@0 | 190 | } catch (NoCollectionKeysSetException e) { |
michael@0 | 191 | // If either default key bundle is not set, we'll say the bundles are not equal. |
michael@0 | 192 | return false; |
michael@0 | 193 | } |
michael@0 | 194 | } |
michael@0 | 195 | } |