mobile/android/base/sync/CollectionKeys.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/CollectionKeys.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,195 @@
     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;
     1.9 +
    1.10 +import java.io.IOException;
    1.11 +import java.io.UnsupportedEncodingException;
    1.12 +import java.util.HashMap;
    1.13 +import java.util.HashSet;
    1.14 +import java.util.Map.Entry;
    1.15 +import java.util.Set;
    1.16 +
    1.17 +import org.json.simple.JSONArray;
    1.18 +import org.json.simple.parser.ParseException;
    1.19 +import org.mozilla.apache.commons.codec.binary.Base64;
    1.20 +import org.mozilla.gecko.sync.crypto.CryptoException;
    1.21 +import org.mozilla.gecko.sync.crypto.KeyBundle;
    1.22 +
    1.23 +public class CollectionKeys {
    1.24 +  private KeyBundle                  defaultKeyBundle     = null;
    1.25 +  private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>();
    1.26 +
    1.27 +  /**
    1.28 +   * Randomly generate a basic CollectionKeys object.
    1.29 +   * @throws CryptoException
    1.30 +   */
    1.31 +  public static CollectionKeys generateCollectionKeys() throws CryptoException {
    1.32 +    CollectionKeys ck = new CollectionKeys();
    1.33 +    ck.clear();
    1.34 +    ck.defaultKeyBundle = KeyBundle.withRandomKeys();
    1.35 +    // TODO: eventually we would like to keep per-collection keys, just generate
    1.36 +    // new ones as appropriate.
    1.37 +    return ck;
    1.38 +  }
    1.39 +
    1.40 +  public KeyBundle defaultKeyBundle() throws NoCollectionKeysSetException {
    1.41 +    if (this.defaultKeyBundle == null) {
    1.42 +      throw new NoCollectionKeysSetException();
    1.43 +    }
    1.44 +    return this.defaultKeyBundle;
    1.45 +  }
    1.46 +
    1.47 +  public boolean keyBundleForCollectionIsNotDefault(String collection) {
    1.48 +    return collectionKeyBundles.containsKey(collection);
    1.49 +  }
    1.50 +
    1.51 +  public KeyBundle keyBundleForCollection(String collection)
    1.52 +      throws NoCollectionKeysSetException {
    1.53 +    if (this.defaultKeyBundle == null) {
    1.54 +      throw new NoCollectionKeysSetException();
    1.55 +    }
    1.56 +    if (keyBundleForCollectionIsNotDefault(collection)) {
    1.57 +      return collectionKeyBundles.get(collection);
    1.58 +    }
    1.59 +    return this.defaultKeyBundle;
    1.60 +  }
    1.61 +
    1.62 +  /**
    1.63 +   * Take a pair of values in a JSON array, handing them off to KeyBundle to
    1.64 +   * produce a usable keypair.
    1.65 +   */
    1.66 +  private static KeyBundle arrayToKeyBundle(JSONArray array) throws UnsupportedEncodingException {
    1.67 +    String encKeyStr  = (String) array.get(0);
    1.68 +    String hmacKeyStr = (String) array.get(1);
    1.69 +    return KeyBundle.fromBase64EncodedKeys(encKeyStr, hmacKeyStr);
    1.70 +  }
    1.71 +
    1.72 +  @SuppressWarnings("unchecked")
    1.73 +  private static JSONArray keyBundleToArray(KeyBundle bundle) {
    1.74 +    // Generate JSON.
    1.75 +    JSONArray keysArray = new JSONArray();
    1.76 +    keysArray.add(new String(Base64.encodeBase64(bundle.getEncryptionKey())));
    1.77 +    keysArray.add(new String(Base64.encodeBase64(bundle.getHMACKey())));
    1.78 +    return keysArray;
    1.79 +  }
    1.80 +
    1.81 +  private ExtendedJSONObject asRecordContents() throws NoCollectionKeysSetException {
    1.82 +    ExtendedJSONObject json = new ExtendedJSONObject();
    1.83 +    json.put("id", "keys");
    1.84 +    json.put("collection", "crypto");
    1.85 +    json.put("default", keyBundleToArray(this.defaultKeyBundle()));
    1.86 +    ExtendedJSONObject colls = new ExtendedJSONObject();
    1.87 +    for (Entry<String, KeyBundle> collKey : collectionKeyBundles.entrySet()) {
    1.88 +      colls.put(collKey.getKey(), keyBundleToArray(collKey.getValue()));
    1.89 +    }
    1.90 +    json.put("collections", colls);
    1.91 +    return json;
    1.92 +  }
    1.93 +
    1.94 +  public CryptoRecord asCryptoRecord() throws NoCollectionKeysSetException {
    1.95 +    ExtendedJSONObject payload = this.asRecordContents();
    1.96 +    CryptoRecord record = new CryptoRecord(payload);
    1.97 +    record.collection = "crypto";
    1.98 +    record.guid       = "keys";
    1.99 +    record.deleted    = false;
   1.100 +    return record;
   1.101 +  }
   1.102 +
   1.103 +  /**
   1.104 +   * Set my key bundle and collection keys with the given key bundle and data
   1.105 +   * (possibly decrypted) from the given record.
   1.106 +   *
   1.107 +   * @param keys
   1.108 +   *          A "crypto/keys" <code>CryptoRecord</code>, encrypted with
   1.109 +   *          <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
   1.110 +   * @param syncKeyBundle
   1.111 +   *          If non-null, the sync key bundle to decrypt <code>keys</code> with.
   1.112 +   */
   1.113 +  public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
   1.114 +      throws CryptoException, IOException, ParseException, NonObjectJSONException {
   1.115 +    if (keys == null) {
   1.116 +      throw new IllegalArgumentException("cannot set key pairs from null record");
   1.117 +    }
   1.118 +    if (syncKeyBundle != null) {
   1.119 +      keys.keyBundle = syncKeyBundle;
   1.120 +      keys.decrypt();
   1.121 +    }
   1.122 +    ExtendedJSONObject cleartext = keys.payload;
   1.123 +    KeyBundle defaultKey = arrayToKeyBundle((JSONArray) cleartext.get("default"));
   1.124 +
   1.125 +    ExtendedJSONObject collections = cleartext.getObject("collections");
   1.126 +    HashMap<String, KeyBundle> collectionKeys = new HashMap<String, KeyBundle>();
   1.127 +    for (Entry<String, Object> pair : collections.entrySet()) {
   1.128 +      KeyBundle bundle = arrayToKeyBundle((JSONArray) pair.getValue());
   1.129 +      collectionKeys.put(pair.getKey(), bundle);
   1.130 +    }
   1.131 +
   1.132 +    this.collectionKeyBundles.clear();
   1.133 +    this.collectionKeyBundles.putAll(collectionKeys);
   1.134 +    this.defaultKeyBundle     = defaultKey;
   1.135 +  }
   1.136 +
   1.137 +  public void setKeyBundleForCollection(String collection, KeyBundle keys) {
   1.138 +    this.collectionKeyBundles.put(collection, keys);
   1.139 +  }
   1.140 +
   1.141 +  public void setDefaultKeyBundle(KeyBundle keys) {
   1.142 +    this.defaultKeyBundle = keys;
   1.143 +  }
   1.144 +
   1.145 +  public void clear() {
   1.146 +    this.defaultKeyBundle = null;
   1.147 +    this.collectionKeyBundles.clear();
   1.148 +  }
   1.149 +
   1.150 +  /**
   1.151 +   * Return set of collections where key is either missing from one collection
   1.152 +   * or not the same in both collections.
   1.153 +   * <p>
   1.154 +   * Does not check for different default keys.
   1.155 +   */
   1.156 +  public static Set<String> differences(CollectionKeys a, CollectionKeys b) {
   1.157 +    Set<String> differences = new HashSet<String>();
   1.158 +    Set<String> collections = new HashSet<String>(a.collectionKeyBundles.keySet());
   1.159 +    collections.addAll(b.collectionKeyBundles.keySet());
   1.160 +
   1.161 +    // Iterate through one collection, collecting missing and differences.
   1.162 +    for (String collection : collections) {
   1.163 +      KeyBundle keyA;
   1.164 +      KeyBundle keyB;
   1.165 +      try {
   1.166 +        keyA = a.keyBundleForCollection(collection); // Will return default key as appropriate.
   1.167 +        keyB = b.keyBundleForCollection(collection); // Will return default key as appropriate.
   1.168 +      } catch (NoCollectionKeysSetException e) {
   1.169 +        differences.add(collection);
   1.170 +        continue;
   1.171 +      }
   1.172 +      // keyA and keyB are not null at this point.
   1.173 +      if (!keyA.equals(keyB)) {
   1.174 +        differences.add(collection);
   1.175 +      }
   1.176 +    }
   1.177 +
   1.178 +    return differences;
   1.179 +  }
   1.180 +
   1.181 +  @Override
   1.182 +  public boolean equals(Object o) {
   1.183 +    if (!(o instanceof CollectionKeys)) {
   1.184 +      return false;
   1.185 +    }
   1.186 +    CollectionKeys other = (CollectionKeys) o;
   1.187 +    try {
   1.188 +      // It would be nice to use map equality here, but there can be map entries
   1.189 +      // where the key is the default key that should compare equal to a missing
   1.190 +      // map entry. Therefore, we always compute the set of differences.
   1.191 +      return defaultKeyBundle().equals(other.defaultKeyBundle()) &&
   1.192 +             CollectionKeys.differences(this, other).isEmpty();
   1.193 +    } catch (NoCollectionKeysSetException e) {
   1.194 +      // If either default key bundle is not set, we'll say the bundles are not equal.
   1.195 +      return false;
   1.196 +    }
   1.197 +  }
   1.198 +}

mercurial