michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.sync; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.UnsupportedEncodingException; michael@0: michael@0: import org.json.simple.JSONObject; michael@0: import org.json.simple.parser.ParseException; michael@0: import org.mozilla.apache.commons.codec.binary.Base64; michael@0: import org.mozilla.gecko.sync.crypto.CryptoException; michael@0: import org.mozilla.gecko.sync.crypto.CryptoInfo; michael@0: import org.mozilla.gecko.sync.crypto.KeyBundle; michael@0: import org.mozilla.gecko.sync.crypto.MissingCryptoInputException; michael@0: import org.mozilla.gecko.sync.crypto.NoKeyBundleException; michael@0: import org.mozilla.gecko.sync.repositories.domain.Record; michael@0: import org.mozilla.gecko.sync.repositories.domain.RecordParseException; michael@0: michael@0: /** michael@0: * A Sync crypto record has: michael@0: * michael@0: * michael@0: * michael@0: * The payload flips between being a blob of JSON with hmac/IV/ciphertext michael@0: * attributes and the cleartext itself. michael@0: * michael@0: * Until there's some benefit to the abstraction, we're simply going to call michael@0: * this CryptoRecord. michael@0: * michael@0: * CryptoRecord uses CryptoInfo to do the actual michael@0: * encryption and decryption. michael@0: */ michael@0: public class CryptoRecord extends Record { michael@0: michael@0: // JSON related constants. michael@0: private static final String KEY_ID = "id"; michael@0: private static final String KEY_COLLECTION = "collection"; michael@0: private static final String KEY_PAYLOAD = "payload"; michael@0: private static final String KEY_MODIFIED = "modified"; michael@0: private static final String KEY_SORTINDEX = "sortindex"; michael@0: private static final String KEY_TTL = "ttl"; michael@0: private static final String KEY_CIPHERTEXT = "ciphertext"; michael@0: private static final String KEY_HMAC = "hmac"; michael@0: private static final String KEY_IV = "IV"; michael@0: michael@0: /** michael@0: * Helper method for doing actual decryption. michael@0: * michael@0: * Input: JSONObject containing a valid payload (cipherText, IV, HMAC), michael@0: * KeyBundle with keys for decryption. Output: byte[] clearText michael@0: * @throws CryptoException michael@0: * @throws UnsupportedEncodingException michael@0: */ michael@0: private static byte[] decryptPayload(ExtendedJSONObject payload, KeyBundle keybundle) throws CryptoException, UnsupportedEncodingException { michael@0: byte[] ciphertext = Base64.decodeBase64(((String) payload.get(KEY_CIPHERTEXT)).getBytes("UTF-8")); michael@0: byte[] iv = Base64.decodeBase64(((String) payload.get(KEY_IV)).getBytes("UTF-8")); michael@0: byte[] hmac = Utils.hex2Byte((String) payload.get(KEY_HMAC)); michael@0: michael@0: return CryptoInfo.decrypt(ciphertext, iv, hmac, keybundle).getMessage(); michael@0: } michael@0: michael@0: // The encrypted JSON body object. michael@0: // The decrypted JSON body object. Fields are copied from `body`. michael@0: michael@0: public ExtendedJSONObject payload; michael@0: public KeyBundle keyBundle; michael@0: michael@0: /** michael@0: * Don't forget to set cleartext or body! michael@0: */ michael@0: public CryptoRecord() { michael@0: super(null, null, 0, false); michael@0: } michael@0: michael@0: public CryptoRecord(ExtendedJSONObject payload) { michael@0: super(null, null, 0, false); michael@0: if (payload == null) { michael@0: throw new IllegalArgumentException( michael@0: "No payload provided to CryptoRecord constructor."); michael@0: } michael@0: this.payload = payload; michael@0: } michael@0: michael@0: public CryptoRecord(String jsonString) throws IOException, ParseException, NonObjectJSONException { michael@0: this(ExtendedJSONObject.parseJSONObject(jsonString)); michael@0: } michael@0: michael@0: /** michael@0: * Create a new CryptoRecord with the same metadata as an existing record. michael@0: * michael@0: * @param source michael@0: */ michael@0: public CryptoRecord(Record source) { michael@0: super(source.guid, source.collection, source.lastModified, source.deleted); michael@0: this.ttl = source.ttl; michael@0: } michael@0: michael@0: @Override michael@0: public Record copyWithIDs(String guid, long androidID) { michael@0: CryptoRecord out = new CryptoRecord(this); michael@0: out.guid = guid; michael@0: out.androidID = androidID; michael@0: out.sortIndex = this.sortIndex; michael@0: out.ttl = this.ttl; michael@0: out.payload = (this.payload == null) ? null : new ExtendedJSONObject(this.payload.object); michael@0: out.keyBundle = this.keyBundle; // TODO: copy me? michael@0: return out; michael@0: } michael@0: michael@0: /** michael@0: * Take a whole record as JSON -- i.e., something like michael@0: * michael@0: * {"payload": "{...}", "id":"foobarbaz"} michael@0: * michael@0: * and turn it into a CryptoRecord object. michael@0: * michael@0: * @param jsonRecord michael@0: * @return michael@0: * A CryptoRecord that encapsulates the provided record. michael@0: * michael@0: * @throws NonObjectJSONException michael@0: * @throws ParseException michael@0: * @throws IOException michael@0: */ michael@0: public static CryptoRecord fromJSONRecord(String jsonRecord) michael@0: throws ParseException, NonObjectJSONException, IOException, RecordParseException { michael@0: byte[] bytes = jsonRecord.getBytes("UTF-8"); michael@0: ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes); michael@0: michael@0: return CryptoRecord.fromJSONRecord(object); michael@0: } michael@0: michael@0: // TODO: defensive programming. michael@0: public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord) michael@0: throws IOException, ParseException, NonObjectJSONException, RecordParseException { michael@0: String id = (String) jsonRecord.get(KEY_ID); michael@0: String collection = (String) jsonRecord.get(KEY_COLLECTION); michael@0: String jsonEncodedPayload = (String) jsonRecord.get(KEY_PAYLOAD); michael@0: michael@0: ExtendedJSONObject payload = ExtendedJSONObject.parseJSONObject(jsonEncodedPayload); michael@0: michael@0: CryptoRecord record = new CryptoRecord(payload); michael@0: record.guid = id; michael@0: record.collection = collection; michael@0: if (jsonRecord.containsKey(KEY_MODIFIED)) { michael@0: Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED); michael@0: if (timestamp == null) { michael@0: throw new RecordParseException("timestamp could not be parsed"); michael@0: } michael@0: record.lastModified = timestamp.longValue(); michael@0: } michael@0: if (jsonRecord.containsKey(KEY_SORTINDEX)) { michael@0: // getLong tries to cast to Long, and might return null. We catch all michael@0: // exceptions, just to be safe. michael@0: try { michael@0: record.sortIndex = jsonRecord.getLong(KEY_SORTINDEX); michael@0: } catch (Exception e) { michael@0: throw new RecordParseException("timestamp could not be parsed"); michael@0: } michael@0: } michael@0: if (jsonRecord.containsKey(KEY_TTL)) { michael@0: // TTLs are never returned by the sync server, so should never be true if michael@0: // the record was fetched. michael@0: try { michael@0: record.ttl = jsonRecord.getLong(KEY_TTL); michael@0: } catch (Exception e) { michael@0: throw new RecordParseException("TTL could not be parsed"); michael@0: } michael@0: } michael@0: // TODO: deleted? michael@0: return record; michael@0: } michael@0: michael@0: public void setKeyBundle(KeyBundle bundle) { michael@0: this.keyBundle = bundle; michael@0: } michael@0: michael@0: public CryptoRecord decrypt() throws CryptoException, IOException, ParseException, michael@0: NonObjectJSONException { michael@0: if (keyBundle == null) { michael@0: throw new NoKeyBundleException(); michael@0: } michael@0: michael@0: // Check that payload contains all pieces for crypto. michael@0: if (!payload.containsKey(KEY_CIPHERTEXT) || michael@0: !payload.containsKey(KEY_IV) || michael@0: !payload.containsKey(KEY_HMAC)) { michael@0: throw new MissingCryptoInputException(); michael@0: } michael@0: michael@0: // There's no difference between handling the crypto/keys object and michael@0: // anything else; we just get this.keyBundle from a different source. michael@0: byte[] cleartext = decryptPayload(payload, keyBundle); michael@0: payload = ExtendedJSONObject.parseUTF8AsJSONObject(cleartext); michael@0: return this; michael@0: } michael@0: michael@0: public CryptoRecord encrypt() throws CryptoException, UnsupportedEncodingException { michael@0: if (this.keyBundle == null) { michael@0: throw new NoKeyBundleException(); michael@0: } michael@0: String cleartext = payload.toJSONString(); michael@0: byte[] cleartextBytes = cleartext.getBytes("UTF-8"); michael@0: CryptoInfo info = CryptoInfo.encrypt(cleartextBytes, keyBundle); michael@0: String message = new String(Base64.encodeBase64(info.getMessage())); michael@0: String iv = new String(Base64.encodeBase64(info.getIV())); michael@0: String hmac = Utils.byte2Hex(info.getHMAC()); michael@0: ExtendedJSONObject ciphertext = new ExtendedJSONObject(); michael@0: ciphertext.put(KEY_CIPHERTEXT, message); michael@0: ciphertext.put(KEY_HMAC, hmac); michael@0: ciphertext.put(KEY_IV, iv); michael@0: this.payload = ciphertext; michael@0: return this; michael@0: } michael@0: michael@0: @Override michael@0: public void initFromEnvelope(CryptoRecord payload) { michael@0: throw new IllegalStateException("Can't do this with a CryptoRecord."); michael@0: } michael@0: michael@0: @Override michael@0: public CryptoRecord getEnvelope() { michael@0: throw new IllegalStateException("Can't do this with a CryptoRecord."); michael@0: } michael@0: michael@0: @Override michael@0: protected void populatePayload(ExtendedJSONObject payload) { michael@0: throw new IllegalStateException("Can't do this with a CryptoRecord."); michael@0: } michael@0: michael@0: @Override michael@0: protected void initFromPayload(ExtendedJSONObject payload) { michael@0: throw new IllegalStateException("Can't do this with a CryptoRecord."); michael@0: } michael@0: michael@0: // TODO: this only works with encrypted object, and has other limitations. michael@0: public JSONObject toJSONObject() { michael@0: ExtendedJSONObject o = new ExtendedJSONObject(); michael@0: o.put(KEY_PAYLOAD, payload.toJSONString()); michael@0: o.put(KEY_ID, this.guid); michael@0: if (this.ttl > 0) { michael@0: o.put(KEY_TTL, this.ttl); michael@0: } michael@0: return o.object; michael@0: } michael@0: michael@0: @Override michael@0: public String toJSONString() { michael@0: return toJSONObject().toJSONString(); michael@0: } michael@0: }