mobile/android/base/sync/CryptoRecord.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     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/. */
     5 package org.mozilla.gecko.sync;
     7 import java.io.IOException;
     8 import java.io.UnsupportedEncodingException;
    10 import org.json.simple.JSONObject;
    11 import org.json.simple.parser.ParseException;
    12 import org.mozilla.apache.commons.codec.binary.Base64;
    13 import org.mozilla.gecko.sync.crypto.CryptoException;
    14 import org.mozilla.gecko.sync.crypto.CryptoInfo;
    15 import org.mozilla.gecko.sync.crypto.KeyBundle;
    16 import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
    17 import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
    18 import org.mozilla.gecko.sync.repositories.domain.Record;
    19 import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
    21 /**
    22  * A Sync crypto record has:
    23  *
    24  * <ul>
    25  * <li>a collection of fields which are not encrypted (id and collection);</il>
    26  * <li>a set of metadata fields (index, modified, ttl);</il>
    27  * <li>a payload, which is encrypted and decrypted on request.</il>
    28  * </ul>
    29  *
    30  * The payload flips between being a blob of JSON with hmac/IV/ciphertext
    31  * attributes and the cleartext itself.
    32  *
    33  * Until there's some benefit to the abstraction, we're simply going to call
    34  * this <code>CryptoRecord</code>.
    35  *
    36  * <code>CryptoRecord</code> uses <code>CryptoInfo</code> to do the actual
    37  * encryption and decryption.
    38  */
    39 public class CryptoRecord extends Record {
    41   // JSON related constants.
    42   private static final String KEY_ID         = "id";
    43   private static final String KEY_COLLECTION = "collection";
    44   private static final String KEY_PAYLOAD    = "payload";
    45   private static final String KEY_MODIFIED   = "modified";
    46   private static final String KEY_SORTINDEX  = "sortindex";
    47   private static final String KEY_TTL        = "ttl";
    48   private static final String KEY_CIPHERTEXT = "ciphertext";
    49   private static final String KEY_HMAC       = "hmac";
    50   private static final String KEY_IV         = "IV";
    52   /**
    53    * Helper method for doing actual decryption.
    54    *
    55    * Input: JSONObject containing a valid payload (cipherText, IV, HMAC),
    56    * KeyBundle with keys for decryption. Output: byte[] clearText
    57    * @throws CryptoException
    58    * @throws UnsupportedEncodingException
    59    */
    60   private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException {
    61     byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8"));
    62     byte[] iv         = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8"));
    63     byte[] hmac       = Utils.hex2Byte((String) payload.get(KEY_HMAC));
    65     return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage();
    66   }
    68   // The encrypted JSON body object.
    69   // The decrypted JSON body object. Fields are copied from `body`.
    71   public ExtendedJSONObject payload;
    72   public KeyBundle   keyBundle;
    74   /**
    75    * Don't forget to set cleartext or body!
    76    */
    77   public CryptoRecord() {
    78     super(null, null, 0, false);
    79   }
    81   public CryptoRecord(ExtendedJSONObject payload) {
    82     super(null, null, 0, false);
    83     if (payload == null) {
    84       throw new IllegalArgumentException(
    85           "No payload provided to CryptoRecord constructor.");
    86     }
    87     this.payload = payload;
    88   }
    90   public CryptoRecord(String jsonString) throws IOException, ParseException, NonObjectJSONException {
    91     this(ExtendedJSONObject.parseJSONObject(jsonString));
    92   }
    94   /**
    95    * Create a new CryptoRecord with the same metadata as an existing record.
    96    *
    97    * @param source
    98    */
    99   public CryptoRecord(Record source) {
   100     super(source.guid, source.collection, source.lastModified, source.deleted);
   101     this.ttl = source.ttl;
   102   }
   104   @Override
   105   public Record copyWithIDs(String guid, long androidID) {
   106     CryptoRecord out = new CryptoRecord(this);
   107     out.guid         = guid;
   108     out.androidID    = androidID;
   109     out.sortIndex    = this.sortIndex;
   110     out.ttl          = this.ttl;
   111     out.payload      = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object);
   112     out.keyBundle    = this.keyBundle;    // TODO: copy me?
   113     return out;
   114   }
   116   /**
   117    * Take a whole record as JSON -- i.e., something like
   118    *
   119    *   {"payload": "{...}", "id":"foobarbaz"}
   120    *
   121    * and turn it into a CryptoRecord object.
   122    *
   123    * @param jsonRecord
   124    * @return
   125    *        A CryptoRecord that encapsulates the provided record.
   126    *
   127    * @throws NonObjectJSONException
   128    * @throws ParseException
   129    * @throws IOException
   130    */
   131   public static CryptoRecord fromJSONRecord(String jsonRecord)
   132       throws ParseException, NonObjectJSONException, IOException, RecordParseException {
   133     byte[] bytes = jsonRecord.getBytes("UTF-8");
   134     ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes);
   136     return CryptoRecord.fromJSONRecord(object);
   137   }
   139   // TODO: defensive programming.
   140   public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord)
   141       throws IOException, ParseException, NonObjectJSONException, RecordParseException {
   142     String id                  = (String) jsonRecord.get(KEY_ID);
   143     String collection          = (String) jsonRecord.get(KEY_COLLECTION);
   144     String jsonEncodedPayload  = (String) jsonRecord.get(KEY_PAYLOAD);
   146     ExtendedJSONObject payload = ExtendedJSONObject.parseJSONObject(jsonEncodedPayload);
   148     CryptoRecord record = new CryptoRecord(payload);
   149     record.guid         = id;
   150     record.collection   = collection;
   151     if (jsonRecord.containsKey(KEY_MODIFIED)) {
   152       Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED);
   153       if (timestamp == null) {
   154         throw new RecordParseException("timestamp could not be parsed");
   155       }
   156       record.lastModified = timestamp.longValue();
   157     }
   158     if (jsonRecord.containsKey(KEY_SORTINDEX)) {
   159       // getLong tries to cast to Long, and might return null. We catch all
   160       // exceptions, just to be safe.
   161       try {
   162         record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX);
   163       } catch (Exception e) {
   164         throw new RecordParseException("timestamp could not be parsed");
   165       }
   166     }
   167     if (jsonRecord.containsKey(KEY_TTL)) {
   168       // TTLs are never returned by the sync server, so should never be true if
   169       // the record was fetched.
   170       try {
   171         record.ttl = jsonRecord.getLong(KEY_TTL);
   172       } catch (Exception e) {
   173         throw new RecordParseException("TTL could not be parsed");
   174       }
   175     }
   176     // TODO: deleted?
   177     return record;
   178   }
   180   public void setKeyBundle(KeyBundle bundle) {
   181     this.keyBundle = bundle;
   182   }
   184   public CryptoRecord decrypt() throws CryptoException, IOException, ParseException,
   185                        NonObjectJSONException {
   186     if (keyBundle == null) {
   187       throw new NoKeyBundleException();
   188     }
   190     // Check that payload contains all pieces for crypto.
   191     if (!payload.containsKey(KEY_CIPHERTEXT) ||
   192         !payload.containsKey(KEY_IV) ||
   193         !payload.containsKey(KEY_HMAC)) {
   194       throw new MissingCryptoInputException();
   195     }
   197     // There's no difference between handling the crypto/keys object and
   198     // anything else; we just get this.keyBundle from a different source.
   199     byte[] cleartext = decryptPayload(payload, keyBundle);
   200     payload = ExtendedJSONObject.parseUTF8AsJSONObject(cleartext);
   201     return this;
   202   }
   204   public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException {
   205     if (this.keyBundle == null) {
   206       throw new NoKeyBundleException();
   207     }
   208     String cleartext = payload.toJSONString();
   209     byte[] cleartextBytes = cleartext.getBytes("UTF-8");
   210     CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle);
   211     String message = new String(Base64.encodeBase64(info.getMessage()));
   212     String iv      = new String(Base64.encodeBase64(info.getIV()));
   213     String hmac    = Utils.byte2Hex(info.getHMAC());
   214     ExtendedJSONObject ciphertext = new ExtendedJSONObject();
   215     ciphertext.put(KEY_CIPHERTEXT, message);
   216     ciphertext.put(KEY_HMAC, hmac);
   217     ciphertext.put(KEY_IV, iv);
   218     this.payload = ciphertext;
   219     return this;
   220   }
   222   @Override
   223   public void initFromEnvelope(CryptoRecord payload) {
   224     throw new IllegalStateException("Can't do this with a CryptoRecord.");
   225   }
   227   @Override
   228   public CryptoRecord getEnvelope() {
   229     throw new IllegalStateException("Can't do this with a CryptoRecord.");
   230   }
   232   @Override
   233   protected void populatePayload(ExtendedJSONObject payload) {
   234     throw new IllegalStateException("Can't do this with a CryptoRecord.");
   235   }
   237   @Override
   238   protected void initFromPayload(ExtendedJSONObject payload) {
   239     throw new IllegalStateException("Can't do this with a CryptoRecord.");
   240   }
   242   // TODO: this only works with encrypted object, and has other limitations.
   243   public JSONObject toJSONObject() {
   244     ExtendedJSONObject o = new ExtendedJSONObject();
   245     o.put(KEY_PAYLOAD, payload.toJSONString());
   246     o.put(KEY_ID,      this.guid);
   247     if (this.ttl > 0) {
   248       o.put(KEY_TTL, this.ttl);
   249     }
   250     return o.object;
   251   }
   253   @Override
   254   public String toJSONString() {
   255     return toJSONObject().toJSONString();
   256   }
   257 }

mercurial