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: *
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: }