/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.android.sync.test; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Arrays; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.ParseException; import org.junit.Test; import org.mozilla.apache.commons.codec.binary.Base64; import org.mozilla.gecko.sync.CryptoRecord; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonArrayJSONException; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.crypto.CryptoException; import org.mozilla.gecko.sync.crypto.KeyBundle; import org.mozilla.gecko.sync.repositories.domain.ClientRecord; import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; import org.mozilla.gecko.sync.repositories.domain.Record; public class TestCryptoRecord { String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q="; String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70="; @Test public void testBaseCryptoRecordEncrypt() throws IOException, ParseException, NonObjectJSONException, CryptoException { ExtendedJSONObject clearPayload = ExtendedJSONObject.parseJSONObject("{\"id\":\"5qRsgXWRJZXr\",\"title\":\"Index of file:///Users/jason/Library/Application Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1,\"date\":1319149012372425}]}"); CryptoRecord record = new CryptoRecord(); record.payload = clearPayload; String expectedGUID = "5qRsgXWRJZXr"; record.guid = expectedGUID; record.keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey); record.encrypt(); assertTrue(record.payload.get("title") == null); assertTrue(record.payload.get("ciphertext") != null); assertEquals(expectedGUID, record.guid); assertEquals(expectedGUID, record.toJSONObject().get("id")); record.decrypt(); assertEquals(expectedGUID, record.toJSONObject().get("id")); } @Test public void testEntireRecord() throws NonObjectJSONException, ParseException, IOException, CryptoException { // Check a raw JSON blob from a real Sync account. String inputString = "{\"sortindex\": 131, \"payload\": \"{\\\"ciphertext\\\":\\\"YJB4dr0vZEIWPirfU2FCJvfzeSLiOP5QWasol2R6ILUxdHsJWuUuvTZVhxYQfTVNou6hVV67jfAvi5Cs+bqhhQsv7icZTiZhPTiTdVGt+uuMotxauVA5OryNGVEZgCCTvT3upzhDFdDbJzVd9O3/gU/b7r/CmAHykX8bTlthlbWeZ8oz6gwHJB5tPRU15nM/m/qW1vyKIw5pw/ZwtAy630AieRehGIGDk+33PWqsfyuT4EUFY9/Ly+8JlnqzxfiBCunIfuXGdLuqTjJOxgrK8mI4wccRFEdFEnmHvh5x7fjl1ID52qumFNQl8zkB75C8XK25alXqwvRR6/AQSP+BgQ==\\\",\\\"IV\\\":\\\"v/0BFgicqYQsd70T39rraA==\\\",\\\"hmac\\\":\\\"59605ed696f6e0e6e062a03510cff742bf6b50d695c042e8372a93f4c2d37dac\\\"}\", \"id\": \"0-P9fabp9vJD\", \"modified\": 1326254123.65}"; CryptoRecord record = CryptoRecord.fromJSONRecord(inputString); assertEquals("0-P9fabp9vJD", record.guid); assertEquals(1326254123650L, record.lastModified); assertEquals(131, record.sortIndex); String b64E = "0A7mU5SZ/tu7ZqwXW1og4qHVHN+zgEi4Xwfwjw+vEJw="; String b64H = "11GN34O9QWXkjR06g8t0gWE1sGgQeWL0qxxWwl8Dmxs="; record.keyBundle = KeyBundle.fromBase64EncodedKeys(b64E, b64H); record.decrypt(); assertEquals("0-P9fabp9vJD", record.guid); assertEquals(1326254123650L, record.lastModified); assertEquals(131, record.sortIndex); assertEquals("Customize Firefox", record.payload.get("title")); assertEquals("0-P9fabp9vJD", record.payload.get("id")); assertTrue(record.payload.get("tags") instanceof JSONArray); } @Test public void testBaseCryptoRecordDecrypt() throws CryptoException, IOException, ParseException, NonObjectJSONException { String base64CipherText = "NMsdnRulLwQsVcwxKW9XwaUe7ouJk5Wn" + "80QhbD80l0HEcZGCynh45qIbeYBik0lg" + "cHbKmlIxTJNwU+OeqipN+/j7MqhjKOGI" + "lvbpiPQQLC6/ffF2vbzL0nzMUuSyvaQz" + "yGGkSYM2xUFt06aNivoQTvU2GgGmUK6M" + "vadoY38hhW2LCMkoZcNfgCqJ26lO1O0s" + "EO6zHsk3IVz6vsKiJ2Hq6VCo7hu123wN" + "egmujHWQSGyf8JeudZjKzfi0OFRRvvm4" + "QAKyBWf0MgrW1F8SFDnVfkq8amCB7Nhd" + "whgLWbN+21NitNwWYknoEWe1m6hmGZDg" + "DT32uxzWxCV8QqqrpH/ZggViEr9uMgoy" + "4lYaWqP7G5WKvvechc62aqnsNEYhH26A" + "5QgzmlNyvB+KPFvPsYzxDnSCjOoRSLx7" + "GG86wT59QZw="; String base64IV = "GX8L37AAb2FZJMzIoXlX8w=="; String base16Hmac = "b1e6c18ac30deb70236bc0d65a46f7a4" + "dce3b8b0e02cf92182b914e3afa5eebc"; ExtendedJSONObject body = new ExtendedJSONObject(); ExtendedJSONObject payload = new ExtendedJSONObject(); payload.put("ciphertext", base64CipherText); payload.put("IV", base64IV); payload.put("hmac", base16Hmac); body.put("payload", payload.toJSONString()); CryptoRecord record = CryptoRecord.fromJSONRecord(body); byte[] decodedKey = Base64.decodeBase64(base64EncryptionKey.getBytes("UTF-8")); byte[] decodedHMAC = Base64.decodeBase64(base64HmacKey.getBytes("UTF-8")); record.keyBundle = new KeyBundle(decodedKey, decodedHMAC); record.decrypt(); String id = (String) record.payload.get("id"); assertTrue(id.equals("5qRsgXWRJZXr")); } @Test public void testBaseCryptoRecordSyncKeyBundle() throws UnsupportedEncodingException, CryptoException { // These values pulled straight out of Firefox. String key = "6m8mv8ex2brqnrmsb9fjuvfg7y"; String user = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd"; // Check our friendly base32 decoding. assertTrue(Arrays.equals(Utils.decodeFriendlyBase32(key), Base64.decodeBase64("8xbKrJfQYwbFkguKmlSm/g==".getBytes("UTF-8")))); KeyBundle bundle = new KeyBundle(user, key); String expectedEncryptKeyBase64 = "/8RzbFT396htpZu5rwgIg2WKfyARgm7dLzsF5pwrVz8="; String expectedHMACKeyBase64 = "NChGjrqoXYyw8vIYP2334cvmMtsjAMUZNqFwV2LGNkM="; byte[] computedEncryptKey = bundle.getEncryptionKey(); byte[] computedHMACKey = bundle.getHMACKey(); assertTrue(Arrays.equals(computedEncryptKey, Base64.decodeBase64(expectedEncryptKeyBase64.getBytes("UTF-8")))); assertTrue(Arrays.equals(computedHMACKey, Base64.decodeBase64(expectedHMACKeyBase64.getBytes("UTF-8")))); } @Test public void testDecrypt() throws CryptoException, NonObjectJSONException, IOException, ParseException { String jsonInput = "{\"sortindex\": 90, \"payload\":" + "\"{\\\"ciphertext\\\":\\\"F4ukf0" + "LM+vhffiKyjaANXeUhfmOPPmQYX1XBoG" + "Rh1LiHeKHB5rqjhzd7yAoxqgmFnkIgQF" + "YPSqRAoCxWiAeGULTX+KM4MU5drbNyR/" + "690JBWSyE1vQSiMGwNIbTKnOLGHKkQVY" + "HDpajg5BNFfvHNQ5Jx7uM9uJcmuEjCI6" + "GRMDKyKjhsTqCd99MONkY5rISutaWQ0e" + "EXFgpA9RZPv4jgWlQhe+YrVnpcrTi20b" + "NgKp3IfIeqEelrZ5FJd2WGZOA021d3e7" + "P3Z4qptefH4Q9/hySrWsELWngBaydyn/" + "IjsheZuKra3kJSST/4SvRZ7qXn\\\",\\" + "\"IV\\\":\\\"GadPajeXhpk75K2YH+L" + "y4w==\\\",\\\"hmac\\\":\\\"71442" + "d946502e3ca475c70a633d3d37f4b4e9" + "313a6d1041d0c0550cd354e7605\\\"}" + "\", \"id\": \"hkZYpC-BH4Xi\", \"" + "modified\": 1320183464.21}"; String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" + "N/G3bz0Bx1M="; String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" + "yUhx+OztVgM="; String expectedDecryptedText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" + "ri\":\"http://hathology.com/2008" + "/06/how-to-edit-your-path-enviro" + "nment-variables-on-mac-os-x/\",\"" + "title\":\"How To Edit Your PATH " + "Environment Variables On Mac OS " + "X\",\"visits\":[{\"date\":131898" + "2074310889,\"type\":1}]}"; KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey); CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput); encrypted.keyBundle = keyBundle; CryptoRecord decrypted = encrypted.decrypt(); // We don't necessarily produce exactly the same JSON but we do have the same values. ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText); assertEquals(expectedJson.get("id"), decrypted.payload.get("id")); assertEquals(expectedJson.get("title"), decrypted.payload.get("title")); assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri")); } @Test public void testEncryptDecrypt() throws CryptoException, NonObjectJSONException, IOException, ParseException { String originalText = "{\"id\":\"hkZYpC-BH4Xi\",\"histU" + "ri\":\"http://hathology.com/2008" + "/06/how-to-edit-your-path-enviro" + "nment-variables-on-mac-os-x/\",\"" + "title\":\"How To Edit Your PATH " + "Environment Variables On Mac OS " + "X\",\"visits\":[{\"date\":131898" + "2074310889,\"type\":1}]}"; String base64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" + "N/G3bz0Bx1M="; String base64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" + "yUhx+OztVgM="; KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey); // Encrypt. CryptoRecord unencrypted = new CryptoRecord(originalText); unencrypted.keyBundle = keyBundle; CryptoRecord encrypted = unencrypted.encrypt(); // Decrypt after round-trip through JSON. CryptoRecord undecrypted = CryptoRecord.fromJSONRecord(encrypted.toJSONString()); undecrypted.keyBundle = keyBundle; CryptoRecord decrypted = undecrypted.decrypt(); // We don't necessarily produce exactly the same JSON but we do have the same values. ExtendedJSONObject expectedJson = new ExtendedJSONObject(originalText); assertEquals(expectedJson.get("id"), decrypted.payload.get("id")); assertEquals(expectedJson.get("title"), decrypted.payload.get("title")); assertEquals(expectedJson.get("histUri"), decrypted.payload.get("histUri")); } @Test public void testDecryptKeysBundle() throws CryptoException, NonObjectJSONException, ParseException, IOException, NonArrayJSONException { String jsonInput = "{\"payload\": \"{\\\"ciphertext\\" + "\":\\\"L1yRyZBkVYKXC1cTpeUqqfmKg" + "CinYV9YntGiG0PfYZSTLQ2s86WPI0VBb" + "QbLZfx7udk6sf6CFE4w5EgiPx0XP3Fbj" + "L7r4qIT0vjbAOrLKedZwA3cgiquc+PXM" + "Etml8B4Dfm0crJK0iROlRkb+lePAYkzI" + "iQn5Ba8mSWQEFoLy3zAcfCYXumA7E0Fj" + "XYD+TqTG5bqYJY4zvPaB9mn9y3WHw==\\" + "\",\\\"IV\\\":\\\"Jjb2oVI5uvvFfm" + "ZYRY4GaA==\\\",\\\"hmac\\\":\\\"" + "0b59731cb1aaedc85f54917b7058f361" + "60826b70050b0d70cd42b0b609b1d717" + "\\\"}\", \"id\": \"keys\", \"mod" + "ified\": 1320183463.91}"; String username = "b6evr62dptbxz7fvebek7btljyu322wp"; String friendlyBase32SyncKey = "basuxv2426eqj7frhvpcwkavdi"; String expectedDecryptedText = "{\"default\":[\"K8fV6PHG8RgugfHe" + "xGesbzTeOs2o12crN/G3bz0Bx1M=\",\"" + "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" + "yUhx+OztVgM=\"],\"collections\":" + "{},\"collection\":\"crypto\",\"i" + "d\":\"keys\"}"; String expectedBase64EncryptionKey = "K8fV6PHG8RgugfHexGesbzTeOs2o12cr" + "N/G3bz0Bx1M="; String expectedBase64HmacKey = "nbceuI6w1RJbBzh+iCJHEs8p4lElsOma" + "yUhx+OztVgM="; KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey); ExtendedJSONObject json = new ExtendedJSONObject(jsonInput); assertEquals("keys", json.get("id")); CryptoRecord encrypted = CryptoRecord.fromJSONRecord(jsonInput); encrypted.keyBundle = syncKeyBundle; CryptoRecord decrypted = encrypted.decrypt(); // We don't necessarily produce exactly the same JSON but we do have the same values. ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText); assertEquals(expectedJson.get("id"), decrypted.payload.get("id")); assertEquals(expectedJson.get("default"), decrypted.payload.get("default")); assertEquals(expectedJson.get("collection"), decrypted.payload.get("collection")); assertEquals(expectedJson.get("collections"), decrypted.payload.get("collections")); // Check that the extracted keys were as expected. JSONArray keys = ExtendedJSONObject.parseJSONObject(decrypted.payload.toJSONString()).getArray("default"); KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys((String)keys.get(0), (String)keys.get(1)); assertArrayEquals(Base64.decodeBase64(expectedBase64EncryptionKey.getBytes("UTF-8")), keyBundle.getEncryptionKey()); assertArrayEquals(Base64.decodeBase64(expectedBase64HmacKey.getBytes("UTF-8")), keyBundle.getHMACKey()); } @Test public void testTTL() throws UnsupportedEncodingException, CryptoException { Record historyRecord = new HistoryRecord(); CryptoRecord cryptoRecord = historyRecord.getEnvelope(); assertEquals(historyRecord.ttl, cryptoRecord.ttl); // Very important that ttls are set in outbound envelopes. JSONObject o = cryptoRecord.toJSONObject(); assertEquals(cryptoRecord.ttl, o.get("ttl")); // Most important of all, outbound encrypted record envelopes. KeyBundle keyBundle = KeyBundle.withRandomKeys(); cryptoRecord.keyBundle = keyBundle; cryptoRecord.encrypt(); assertEquals(historyRecord.ttl, cryptoRecord.ttl); // Should be preserved. o = cryptoRecord.toJSONObject(); assertEquals(cryptoRecord.ttl, o.get("ttl")); // But we should ignore negative ttls. Record clientRecord = new ClientRecord(); clientRecord.ttl = -1; // Don't ttl this record. o = clientRecord.getEnvelope().toJSONObject(); assertNull(o.get("ttl")); // But we should ignore negative ttls in outbound encrypted record envelopes. cryptoRecord = clientRecord.getEnvelope(); cryptoRecord.keyBundle = keyBundle; cryptoRecord.encrypt(); o = cryptoRecord.toJSONObject(); assertNull(o.get("ttl")); } }