mobile/android/base/sync/CryptoRecord.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/sync/CryptoRecord.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,257 @@
     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 +
    1.13 +import org.json.simple.JSONObject;
    1.14 +import org.json.simple.parser.ParseException;
    1.15 +import org.mozilla.apache.commons.codec.binary.Base64;
    1.16 +import org.mozilla.gecko.sync.crypto.CryptoException;
    1.17 +import org.mozilla.gecko.sync.crypto.CryptoInfo;
    1.18 +import org.mozilla.gecko.sync.crypto.KeyBundle;
    1.19 +import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
    1.20 +import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
    1.21 +import org.mozilla.gecko.sync.repositories.domain.Record;
    1.22 +import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
    1.23 +
    1.24 +/**
    1.25 + * A Sync crypto record has:
    1.26 + *
    1.27 + * <ul>
    1.28 + * <li>a collection of fields which are not encrypted (id and collection);</il>
    1.29 + * <li>a set of metadata fields (index, modified, ttl);</il>
    1.30 + * <li>a payload, which is encrypted and decrypted on request.</il>
    1.31 + * </ul>
    1.32 + *
    1.33 + * The payload flips between being a blob of JSON with hmac/IV/ciphertext
    1.34 + * attributes and the cleartext itself.
    1.35 + *
    1.36 + * Until there's some benefit to the abstraction, we're simply going to call
    1.37 + * this <code>CryptoRecord</code>.
    1.38 + *
    1.39 + * <code>CryptoRecord</code> uses <code>CryptoInfo</code> to do the actual
    1.40 + * encryption and decryption.
    1.41 + */
    1.42 +public class CryptoRecord extends Record {
    1.43 +
    1.44 +  // JSON related constants.
    1.45 +  private static final String KEY_ID         = "id";
    1.46 +  private static final String KEY_COLLECTION = "collection";
    1.47 +  private static final String KEY_PAYLOAD    = "payload";
    1.48 +  private static final String KEY_MODIFIED   = "modified";
    1.49 +  private static final String KEY_SORTINDEX  = "sortindex";
    1.50 +  private static final String KEY_TTL        = "ttl";
    1.51 +  private static final String KEY_CIPHERTEXT = "ciphertext";
    1.52 +  private static final String KEY_HMAC       = "hmac";
    1.53 +  private static final String KEY_IV         = "IV";
    1.54 +
    1.55 +  /**
    1.56 +   * Helper method for doing actual decryption.
    1.57 +   *
    1.58 +   * Input: JSONObject containing a valid payload (cipherText, IV, HMAC),
    1.59 +   * KeyBundle with keys for decryption. Output: byte[] clearText
    1.60 +   * @throws CryptoException
    1.61 +   * @throws UnsupportedEncodingException
    1.62 +   */
    1.63 +  private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException {
    1.64 +    byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8"));
    1.65 +    byte[] iv         = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8"));
    1.66 +    byte[] hmac       = Utils.hex2Byte((String) payload.get(KEY_HMAC));
    1.67 +
    1.68 +    return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage();
    1.69 +  }
    1.70 +
    1.71 +  // The encrypted JSON body object.
    1.72 +  // The decrypted JSON body object. Fields are copied from `body`.
    1.73 +
    1.74 +  public ExtendedJSONObject payload;
    1.75 +  public KeyBundle   keyBundle;
    1.76 +
    1.77 +  /**
    1.78 +   * Don't forget to set cleartext or body!
    1.79 +   */
    1.80 +  public CryptoRecord() {
    1.81 +    super(null, null, 0, false);
    1.82 +  }
    1.83 +
    1.84 +  public CryptoRecord(ExtendedJSONObject payload) {
    1.85 +    super(null, null, 0, false);
    1.86 +    if (payload == null) {
    1.87 +      throw new IllegalArgumentException(
    1.88 +          "No payload provided to CryptoRecord constructor.");
    1.89 +    }
    1.90 +    this.payload = payload;
    1.91 +  }
    1.92 +
    1.93 +  public CryptoRecord(String jsonString) throws IOException, ParseException, NonObjectJSONException {
    1.94 +    this(ExtendedJSONObject.parseJSONObject(jsonString));
    1.95 +  }
    1.96 +
    1.97 +  /**
    1.98 +   * Create a new CryptoRecord with the same metadata as an existing record.
    1.99 +   *
   1.100 +   * @param source
   1.101 +   */
   1.102 +  public CryptoRecord(Record source) {
   1.103 +    super(source.guid, source.collection, source.lastModified, source.deleted);
   1.104 +    this.ttl = source.ttl;
   1.105 +  }
   1.106 +
   1.107 +  @Override
   1.108 +  public Record copyWithIDs(String guid, long androidID) {
   1.109 +    CryptoRecord out = new CryptoRecord(this);
   1.110 +    out.guid         = guid;
   1.111 +    out.androidID    = androidID;
   1.112 +    out.sortIndex    = this.sortIndex;
   1.113 +    out.ttl          = this.ttl;
   1.114 +    out.payload      = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object);
   1.115 +    out.keyBundle    = this.keyBundle;    // TODO: copy me?
   1.116 +    return out;
   1.117 +  }
   1.118 +
   1.119 +  /**
   1.120 +   * Take a whole record as JSON -- i.e., something like
   1.121 +   *
   1.122 +   *   {"payload": "{...}", "id":"foobarbaz"}
   1.123 +   *
   1.124 +   * and turn it into a CryptoRecord object.
   1.125 +   *
   1.126 +   * @param jsonRecord
   1.127 +   * @return
   1.128 +   *        A CryptoRecord that encapsulates the provided record.
   1.129 +   *
   1.130 +   * @throws NonObjectJSONException
   1.131 +   * @throws ParseException
   1.132 +   * @throws IOException
   1.133 +   */
   1.134 +  public static CryptoRecord fromJSONRecord(String jsonRecord)
   1.135 +      throws ParseException, NonObjectJSONException, IOException, RecordParseException {
   1.136 +    byte[] bytes = jsonRecord.getBytes("UTF-8");
   1.137 +    ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes);
   1.138 +
   1.139 +    return CryptoRecord.fromJSONRecord(object);
   1.140 +  }
   1.141 +
   1.142 +  // TODO: defensive programming.
   1.143 +  public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord)
   1.144 +      throws IOException, ParseException, NonObjectJSONException, RecordParseException {
   1.145 +    String id                  = (String) jsonRecord.get(KEY_ID);
   1.146 +    String collection          = (String) jsonRecord.get(KEY_COLLECTION);
   1.147 +    String jsonEncodedPayload  = (String) jsonRecord.get(KEY_PAYLOAD);
   1.148 +
   1.149 +    ExtendedJSONObject payload = ExtendedJSONObject.parseJSONObject(jsonEncodedPayload);
   1.150 +
   1.151 +    CryptoRecord record = new CryptoRecord(payload);
   1.152 +    record.guid         = id;
   1.153 +    record.collection   = collection;
   1.154 +    if (jsonRecord.containsKey(KEY_MODIFIED)) {
   1.155 +      Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED);
   1.156 +      if (timestamp == null) {
   1.157 +        throw new RecordParseException("timestamp could not be parsed");
   1.158 +      }
   1.159 +      record.lastModified = timestamp.longValue();
   1.160 +    }
   1.161 +    if (jsonRecord.containsKey(KEY_SORTINDEX)) {
   1.162 +      // getLong tries to cast to Long, and might return null. We catch all
   1.163 +      // exceptions, just to be safe.
   1.164 +      try {
   1.165 +        record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX);
   1.166 +      } catch (Exception e) {
   1.167 +        throw new RecordParseException("timestamp could not be parsed");
   1.168 +      }
   1.169 +    }
   1.170 +    if (jsonRecord.containsKey(KEY_TTL)) {
   1.171 +      // TTLs are never returned by the sync server, so should never be true if
   1.172 +      // the record was fetched.
   1.173 +      try {
   1.174 +        record.ttl = jsonRecord.getLong(KEY_TTL);
   1.175 +      } catch (Exception e) {
   1.176 +        throw new RecordParseException("TTL could not be parsed");
   1.177 +      }
   1.178 +    }
   1.179 +    // TODO: deleted?
   1.180 +    return record;
   1.181 +  }
   1.182 +
   1.183 +  public void setKeyBundle(KeyBundle bundle) {
   1.184 +    this.keyBundle = bundle;
   1.185 +  }
   1.186 +
   1.187 +  public CryptoRecord decrypt() throws CryptoException, IOException, ParseException,
   1.188 +                       NonObjectJSONException {
   1.189 +    if (keyBundle == null) {
   1.190 +      throw new NoKeyBundleException();
   1.191 +    }
   1.192 +
   1.193 +    // Check that payload contains all pieces for crypto.
   1.194 +    if (!payload.containsKey(KEY_CIPHERTEXT) ||
   1.195 +        !payload.containsKey(KEY_IV) ||
   1.196 +        !payload.containsKey(KEY_HMAC)) {
   1.197 +      throw new MissingCryptoInputException();
   1.198 +    }
   1.199 +
   1.200 +    // There's no difference between handling the crypto/keys object and
   1.201 +    // anything else; we just get this.keyBundle from a different source.
   1.202 +    byte[] cleartext = decryptPayload(payload, keyBundle);
   1.203 +    payload = ExtendedJSONObject.parseUTF8AsJSONObject(cleartext);
   1.204 +    return this;
   1.205 +  }
   1.206 +
   1.207 +  public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException {
   1.208 +    if (this.keyBundle == null) {
   1.209 +      throw new NoKeyBundleException();
   1.210 +    }
   1.211 +    String cleartext = payload.toJSONString();
   1.212 +    byte[] cleartextBytes = cleartext.getBytes("UTF-8");
   1.213 +    CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle);
   1.214 +    String message = new String(Base64.encodeBase64(info.getMessage()));
   1.215 +    String iv      = new String(Base64.encodeBase64(info.getIV()));
   1.216 +    String hmac    = Utils.byte2Hex(info.getHMAC());
   1.217 +    ExtendedJSONObject ciphertext = new ExtendedJSONObject();
   1.218 +    ciphertext.put(KEY_CIPHERTEXT, message);
   1.219 +    ciphertext.put(KEY_HMAC, hmac);
   1.220 +    ciphertext.put(KEY_IV, iv);
   1.221 +    this.payload = ciphertext;
   1.222 +    return this;
   1.223 +  }
   1.224 +
   1.225 +  @Override
   1.226 +  public void initFromEnvelope(CryptoRecord payload) {
   1.227 +    throw new IllegalStateException("Can't do this with a CryptoRecord.");
   1.228 +  }
   1.229 +
   1.230 +  @Override
   1.231 +  public CryptoRecord getEnvelope() {
   1.232 +    throw new IllegalStateException("Can't do this with a CryptoRecord.");
   1.233 +  }
   1.234 +
   1.235 +  @Override
   1.236 +  protected void populatePayload(ExtendedJSONObject payload) {
   1.237 +    throw new IllegalStateException("Can't do this with a CryptoRecord.");
   1.238 +  }
   1.239 +
   1.240 +  @Override
   1.241 +  protected void initFromPayload(ExtendedJSONObject payload) {
   1.242 +    throw new IllegalStateException("Can't do this with a CryptoRecord.");
   1.243 +  }
   1.244 +
   1.245 +  // TODO: this only works with encrypted object, and has other limitations.
   1.246 +  public JSONObject toJSONObject() {
   1.247 +    ExtendedJSONObject o = new ExtendedJSONObject();
   1.248 +    o.put(KEY_PAYLOAD, payload.toJSONString());
   1.249 +    o.put(KEY_ID,      this.guid);
   1.250 +    if (this.ttl > 0) {
   1.251 +      o.put(KEY_TTL, this.ttl);
   1.252 +    }
   1.253 +    return o.object;
   1.254 +  }
   1.255 +
   1.256 +  @Override
   1.257 +  public String toJSONString() {
   1.258 +    return toJSONObject().toJSONString();
   1.259 +  }
   1.260 +}

mercurial