Bug 1486954 - Part III, Upgrade existing Nightly credit card records to OSKeyStore r=MattN
authorTimothy Guan-tin Chien <timdream@gmail.com>
Wed, 17 Oct 2018 02:33:02 +0000
changeset 500035 27e9286503e8bfbb61ed2edfc374257b728fec6b
parent 500034 87e64652386dd1fc08ebcc4ba7f1405805de20a1
child 500036 c895888bdddc97e36675e49844e7c115e0d6cc12
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1486954
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1486954 - Part III, Upgrade existing Nightly credit card records to OSKeyStore r=MattN For Nightly users who already have credit cards saved in their profile, we will do a one-off upgrade of their encrypted credit card number. This only applies to users who have NO master password set, to avoid showing them the master password prompt when we migrate. For those who did, we would quietly delete their credit card record from the store. Differential Revision: https://phabricator.services.mozilla.com/D8029
browser/components/payments/res/debugging.js
browser/extensions/formautofill/FormAutofillStorage.jsm
browser/extensions/formautofill/test/unit/test_creditCardRecords.js
browser/extensions/formautofill/test/unit/test_migrateRecords.js
browser/extensions/formautofill/test/unit/test_reconcile.js
--- a/browser/components/payments/res/debugging.js
+++ b/browser/components/payments/res/debugging.js
@@ -313,17 +313,17 @@ let DUPED_ADDRESSES = {
 };
 
 let BASIC_CARDS_1 = {
   "53f9d009aed2": {
     billingAddressGUID: "68gjdh354j",
     methodName: "basic-card",
     "cc-number": "************5461",
     "guid": "53f9d009aed2",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1505240896213,
     "timeLastModified": 1515609524588,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "John Smith",
     "cc-exp-month": 6,
     "cc-exp-year": 2024,
     "cc-type": "visa",
@@ -331,17 +331,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Smith",
     "cc-exp": "2024-06",
   },
   "9h5d4h6f4d1s": {
     methodName: "basic-card",
     "cc-number": "************0954",
     "guid": "9h5d4h6f4d1s",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Doe",
     "cc-exp-month": 5,
     "cc-exp-year": 2023,
     "cc-type": "mastercard",
@@ -349,17 +349,17 @@ let BASIC_CARDS_1 = {
     "cc-additional-name": "",
     "cc-family-name": "Doe",
     "cc-exp": "2023-05",
   },
   "123456789abc": {
     methodName: "basic-card",
     "cc-number": "************1234",
     "guid": "123456789abc",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-name": "Jane Fields",
     "cc-given-name": "Jane",
     "cc-additional-name": "",
     "cc-family-name": "Fields",
@@ -383,17 +383,17 @@ let BASIC_CARDS_1 = {
     "cc-exp-month": 6,
     "cc-exp-year": 2023,
     "cc-exp": "2023-06",
   },
   "missing-cc-name": {
     methodName: "basic-card",
     "cc-number": "************8563",
     "guid": "missing-cc-name",
-    "version": 1,
+    "version": 2,
     "timeCreated": 1517890536491,
     "timeLastModified": 1517890564518,
     "timeLastUsed": 0,
     "timesUsed": 0,
     "cc-exp-month": 8,
     "cc-exp-year": 2024,
     "cc-exp": "2024-08",
   },
--- a/browser/extensions/formautofill/FormAutofillStorage.jsm
+++ b/browser/extensions/formautofill/FormAutofillStorage.jsm
@@ -142,28 +142,31 @@ ChromeUtils.defineModuleGetter(this, "Fo
                                "resource://formautofill/FormAutofillNameUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                                "resource://formautofill/FormAutofillUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "OSKeyStore",
                                "resource://formautofill/OSKeyStore.jsm");
 ChromeUtils.defineModuleGetter(this, "PhoneNumber",
                                "resource://formautofill/phonenumberutils/PhoneNumber.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
+                                   "@mozilla.org/login-manager/crypto/SDR;1",
+                                   Ci.nsILoginManagerCrypto);
 XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                           "nsICryptoHash", "initWithString");
 
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const VALID_ADDRESS_FIELDS = [
   "given-name",
   "additional-name",
   "family-name",
   "organization",
   "street-address",
   "address-level3",
@@ -259,23 +262,24 @@ class AutofillRecords {
 
     this.VALID_FIELDS = validFields;
     this.VALID_COMPUTED_FIELDS = validComputedFields;
 
     this._store = store;
     this._collectionName = collectionName;
     this._schemaVersion = schemaVersion;
 
-    Promise.all(this._data.map(record => this._migrateRecord(record)))
-      .then(hasChangesArr => {
-        let dataHasChanges = hasChangesArr.find(hasChanges => hasChanges);
-        if (dataHasChanges) {
-          this._store.saveSoon();
-        }
-      });
+    this._initializePromise =
+      Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
+        .then(hasChangesArr => {
+          let dataHasChanges = hasChangesArr.includes(true);
+          if (dataHasChanges) {
+            this._store.saveSoon();
+          }
+        });
   }
 
   /**
    * Gets the schema version number.
    *
    * @returns {number}
    *          The current schema version number.
    */
@@ -299,16 +303,24 @@ class AutofillRecords {
   _ensureMatchingVersion(record) {
     if (record.version != this.version) {
       throw new Error(`Got unknown record version ${
         record.version}; want ${this.version}`);
     }
   }
 
   /**
+   * Initialize the records in the collection, resolves when the migration completes.
+   * @returns {Promise}
+   */
+  initialize() {
+    return this._initializePromise;
+  }
+
+  /**
    * Adds a new record.
    *
    * @param {Object} record
    *        The new record for saving.
    * @param {boolean} [options.sourceSync = false]
    *        Did sync generate this addition?
    * @returns {Promise<string>}
    *          The GUID of the newly added item..
@@ -1171,36 +1183,47 @@ class AutofillRecords {
   }
 
   _findIndexByGUID(guid, {includeDeleted = false} = {}) {
     return this._data.findIndex(record => {
       return record.guid == guid && (!record.deleted || includeDeleted);
     });
   }
 
-  async _migrateRecord(record) {
+  async _migrateRecord(record, index) {
     let hasChanges = false;
 
     if (record.deleted) {
       return hasChanges;
     }
 
     if (!record.version || isNaN(record.version) || record.version < 1) {
       this.log.warn("Invalid record version:", record.version);
 
       // Force to run the migration.
       record.version = 0;
     }
 
     if (record.version < this.version) {
       hasChanges = true;
-      record.version = this.version;
+
+      record = await this._computeMigratedRecord(record);
 
-      // Force to recompute fields if we upgrade the schema.
-      await this._stripComputedFields(record);
+      if (record.deleted) {
+        // record is deleted by _computeMigratedRecord(),
+        // go ahead and put it in the store.
+        this._data[index] = record;
+        return hasChanges;
+      }
+
+      // Compute the computed fields before putting it to store.
+      await this.computeFields(record);
+      this._data[index] = record;
+
+      return hasChanges;
     }
 
     hasChanges |= await this.computeFields(record);
     return hasChanges;
   }
 
   _normalizeRecord(record, preserveEmptyFields = false) {
     this._normalizeFields(record);
@@ -1251,16 +1274,34 @@ class AutofillRecords {
     this._store.data[this._collectionName] = [];
     this._store.saveSoon();
     Services.obs.notifyObservers({wrappedJSObject: {
       sourceSync,
       collectionName: this._collectionName,
     }}, "formautofill-storage-changed", "removeAll");
   }
 
+  /**
+   * Strip the computed fields based on the record version.
+   * @param   {Object} record      The record to migrate
+   * @returns {Object}             Migrated record.
+   *                               Record is always cloned, with version updated,
+   *                               with computed fields stripped.
+   *                               Could be a tombstone record, if the record
+   *                               should be discorded.
+   */
+  async _computeMigratedRecord(record) {
+    if (!record.deleted) {
+      record = this._clone(record);
+      await this._stripComputedFields(record);
+      record.version = this.version;
+    }
+    return record;
+  }
+
   async _stripComputedFields(record) {
     this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
@@ -1608,16 +1649,61 @@ class CreditCards extends AutofillRecord
       } else {
         creditCard["cc-number-encrypted"] = "";
       }
     }
 
     return hasNewComputedFields;
   }
 
+  async _computeMigratedRecord(creditCard) {
+    if (creditCard["cc-number-encrypted"]) {
+      switch (creditCard.version) {
+        case 1: {
+          if (!cryptoSDR.isLoggedIn) {
+            // We cannot decrypt the data, so silently remove the record for
+            // the user.
+            if (creditCard.deleted) {
+              break;
+            }
+
+            this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);
+
+            // Replace the record with a tombstone record here,
+            // regardless of existence of sync metadata.
+            let existingSync = this._getSyncMetaData(creditCard);
+            creditCard = {
+              guid: creditCard.guid,
+              timeLastModified: Date.now(),
+              deleted: true,
+            };
+
+            if (existingSync) {
+              creditCard._sync = existingSync;
+              existingSync.changeCounter++;
+            }
+            break;
+          }
+
+          creditCard = this._clone(creditCard);
+
+          // Decrypt the cc-number using version 1 encryption.
+          let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
+          // Re-encrypt the cc-number with version 2 encryption.
+          creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
+          break;
+        }
+
+        default:
+          throw new Error("Unknown credit card version to migrate: " + creditCard.version);
+      }
+    }
+    return super._computeMigratedRecord(creditCard);
+  }
+
   async _stripComputedFields(creditCard) {
     if (creditCard["cc-number-encrypted"]) {
       creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
     }
     await super._stripComputedFields(creditCard);
   }
 
   _normalizeFields(creditCard) {
@@ -1671,16 +1757,43 @@ class CreditCards extends AutofillRecord
   }
 
   _validateFields(creditCard) {
     if (!creditCard["cc-number"]) {
       throw new Error("Missing/invalid cc-number");
     }
   }
 
+  _ensureMatchingVersion(record) {
+    if (!record.version || isNaN(record.version) || record.version < 1) {
+      throw new Error(`Got invalid record version ${
+        record.version}; want ${this.version}`);
+    }
+
+    if (record.version < this.version) {
+      switch (record.version) {
+        case 1:
+          // The difference between version 1 and 2 is only about the encryption
+          // method used for the cc-number-encrypted field.
+          // As long as the record is already decrypted, it is safe to bump the
+          // version directly.
+          if (!record["cc-number-encrypted"]) {
+            record.version = this.version;
+          } else {
+            throw new Error("Unexpected record migration path.");
+          }
+          break;
+        default:
+          throw new Error("Unknown credit card version to match: " + record.version);
+      }
+    }
+
+    return super._ensureMatchingVersion(record);
+  }
+
   /**
    * Normalize the given record and return the first matched guid if storage has the same record.
    * @param {Object} targetCreditCard
    *        The credit card for duplication checking.
    * @returns {Promise<string|null>}
    *          Return the first guid if storage has the same credit card and null otherwise.
    */
   async getDuplicateGuid(targetCreditCard) {
@@ -1797,17 +1910,20 @@ FormAutofillStorage.prototype = {
    * @rejects  JavaScript exception.
    */
   initialize() {
     if (!this._initializePromise) {
       this._store = new JSONFile({
         path: this._path,
         dataPostProcessor: this._dataPostProcessor.bind(this),
       });
-      this._initializePromise = this._store.load();
+      this._initializePromise = this._store.load()
+        .then(() => Promise.all([
+          this.addresses.initialize(),
+          this.creditCards.initialize()]));
     }
     return this._initializePromise;
   },
 
   _dataPostProcessor(data) {
     data.version = this.version;
     if (!data.addresses) {
       data.addresses = [];
--- a/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_creditCardRecords.js
@@ -311,17 +311,17 @@ add_task(async function test_add() {
   let creditCards = await profileStorage.creditCards.getAll();
 
   Assert.equal(creditCards.length, 2);
 
   do_check_credit_card_matches(creditCards[0], TEST_CREDIT_CARD_1);
   do_check_credit_card_matches(creditCards[1], TEST_CREDIT_CARD_2);
 
   Assert.notEqual(creditCards[0].guid, undefined);
-  Assert.equal(creditCards[0].version, 1);
+  Assert.equal(creditCards[0].version, 2);
   Assert.notEqual(creditCards[0].timeCreated, undefined);
   Assert.equal(creditCards[0].timeLastModified, creditCards[0].timeCreated);
   Assert.equal(creditCards[0].timeLastUsed, 0);
   Assert.equal(creditCards[0].timesUsed, 0);
 
   // Empty string should be deleted before saving.
   await profileStorage.creditCards.add(TEST_CREDIT_CARD_WITH_EMPTY_FIELD);
   let creditCard = profileStorage.creditCards._data[2];
--- a/browser/extensions/formautofill/test/unit/test_migrateRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_migrateRecords.js
@@ -1,24 +1,27 @@
 /**
  * Tests the migration algorithm in profileStorage.
  */
 
 "use strict";
 
+ChromeUtils.import("resource://testing-common/LoginTestUtils.jsm", this);
+
 let FormAutofillStorage;
-
+let OSKeyStore;
 add_task(async function setup() {
   ({FormAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {}));
+  ({OSKeyStore} = ChromeUtils.import("resource://formautofill/OSKeyStore.jsm", {}));
 });
 
 const TEST_STORE_FILE_NAME = "test-profile.json";
 
 const ADDRESS_SCHEMA_VERSION = 1;
-const CREDIT_CARD_SCHEMA_VERSION = 1;
+const CREDIT_CARD_SCHEMA_VERSION = 2;
 
 const ADDRESS_TESTCASES = [
   {
     description: "The record version is equal to the current version. The migration shouldn't be invoked.",
     record: {
       guid: "test-guid",
       version: ADDRESS_SCHEMA_VERSION,
       "given-name": "Timothy",
@@ -241,27 +244,98 @@ let do_check_record_matches = (expectedR
 };
 
 add_task(async function test_migrateAddressRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(ADDRESS_TESTCASES.map(async testcase => {
+  for (let testcase of ADDRESS_TESTCASES) {
     info(testcase.description);
-    await profileStorage.addresses._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.addresses = [testcase.record];
+    await profileStorage.addresses._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.addresses._data[0]);
+  }
 });
 
 add_task(async function test_migrateCreditCardRecords() {
   let path = getTempFile(TEST_STORE_FILE_NAME).path;
 
   let profileStorage = new FormAutofillStorage(path);
   await profileStorage.initialize();
 
-  await Promise.all(CREDIT_CARD_TESTCASES.map(async testcase => {
+  for (let testcase of CREDIT_CARD_TESTCASES) {
     info(testcase.description);
-    await profileStorage.creditCards._migrateRecord(testcase.record);
-    do_check_record_matches(testcase.expectedResult, testcase.record);
-  }));
+    profileStorage._store.data.creditCards = [testcase.record];
+    await profileStorage.creditCards._migrateRecord(testcase.record, 0);
+    do_check_record_matches(testcase.expectedResult, profileStorage.creditCards._data[0]);
+  }
 });
+
+add_task(async function test_migrateEncryptedCreditCardNumber() {
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  const ccNumber = "4111111111111111";
+  let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"]
+    .createInstance(Ci.nsILoginManagerCrypto);
+
+  info("Encrypted credit card should be migrated from v1 to v2");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": cryptoSDR.encrypt(ccNumber),
+  };
+
+  let expectedRecord = {
+    guid: "test-guid",
+    version: CREDIT_CARD_SCHEMA_VERSION,
+    "cc-name": "Timothy",
+    "cc-given-name": "Timothy",
+  };
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(expectedRecord.guid, record.guid);
+  Assert.equal(expectedRecord.version, record.version);
+  Assert.equal(expectedRecord["cc-name"], record["cc-name"]);
+  Assert.equal(expectedRecord["cc-given-name"], record["cc-given-name"]);
+
+  // Ciphertext of OS Key Store is not stable, must compare decrypted text here.
+  Assert.equal(ccNumber, await OSKeyStore.decrypt(record["cc-number-encrypted"]));
+});
+
+add_task(async function test_migrateEncryptedCreditCardNumberWithMP() {
+  LoginTestUtils.masterPassword.enable();
+
+  let path = getTempFile(TEST_STORE_FILE_NAME).path;
+
+  let profileStorage = new FormAutofillStorage(path);
+  await profileStorage.initialize();
+
+  info("Encrypted credit card should be migrated a tombstone if MP is enabled");
+
+  let record = {
+    guid: "test-guid",
+    version: 1,
+    "cc-name": "Timothy",
+    "cc-number-encrypted": "(encrypted to be discarded)",
+  };
+
+  profileStorage._store.data.creditCards = [record];
+  await profileStorage.creditCards._migrateRecord(record, 0);
+  record = profileStorage.creditCards._data[0];
+
+  Assert.equal(record.guid, "test-guid");
+  Assert.equal(record.deleted, true);
+  Assert.equal(typeof record.version, "undefined");
+  Assert.equal(typeof record["cc-name"], "undefined");
+  Assert.equal(typeof record["cc-number-encrypted"], "undefined");
+
+  LoginTestUtils.masterPassword.disable();
+});
+
--- a/browser/extensions/formautofill/test/unit/test_reconcile.js
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -465,220 +465,220 @@ const ADDRESS_RECONCILE_TESTCASES = [
 ];
 
 const CREDIT_CARD_RECONCILE_TESTCASES = [
   {
     description: "Local change",
     parent: {
       // So when we last wrote the record to the server, it had these values.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current local record - by comparing against parent we can see that
       // only the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       // This is the incoming record. It has the same values as "parent", so
       // we can deduce the record hasn't actually been changed remotely so we
       // can safely ignore the incoming record and write our local changes.
       "guid": "2bbd2d8fbc6b",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "2bbd2d8fbc6b",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Remote change",
     parent: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "e3680e9f890d",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "e3680e9f890d",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
 
   {
     description: "New local field",
     parent: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "0cba738b1be0",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "0cba738b1be0",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "New remote field",
     parent: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "be3ef97f8285",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "be3ef97f8285",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
   },
   {
     description: "Deleted field locally",
     parent: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "9627322248ec",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     reconciled: {
       "guid": "9627322248ec",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Deleted field remotely",
     parent: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "7d7509f3eeb2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     reconciled: {
       "guid": "7d7509f3eeb2",
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
   },
   {
     description: "Local and remote changes to unrelated fields",
     parent: {
       // The last time we wrote this to the server, "cc-exp-month" was 12.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       // The current local record - so locally we've changed "cc-number".
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 12,
     }],
     remote: {
       // Remotely, we've changed "cc-exp-month" to 1.
       "guid": "e087a06dfc57",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 1,
     },
     reconciled: {
       "guid": "e087a06dfc57",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 1,
     },
   },
   {
     description: "Multiple local changes",
     parent: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
     }, {
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     }],
     remote: {
       "guid": "340a078c596f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-year": 2000,
     },
     reconciled: {
       "guid": "340a078c596f",
       "cc-name": "Skip",
       "cc-number": "4111111111111111",
@@ -687,54 +687,54 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local and remote diverged from the shared parent, but the values are the
     // same, so we shouldn't fork.
     description: "Same change to local and remote",
     parent: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "0b3a72a1bea2",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     reconciled: {
       "guid": "0b3a72a1bea2",
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to single field",
     parent: {
       // This is what we last wrote to the sync server.
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     local: [{
       // The current version of the local record - the cc-number has changed locally.
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
     }],
     remote: {
       // An incoming record has a different cc-number than any of the above!
       "guid": "62068784d089",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
     forked: {
       // So we've forked the local record to a new GUID (and the next sync is
       // going to write this as a new record)
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
@@ -745,29 +745,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     },
   },
   {
     description: "Conflicting changes to multiple fields",
     parent: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
     }],
     remote: {
       "guid": "244dbb692e94",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "5103059495477870",
       "cc-exp-month": 1,
@@ -778,28 +778,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4929001587121045",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field deleted locally, changed remotely",
     parent: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     }],
     remote: {
       "guid": "6fc45e03d19a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
@@ -809,29 +809,29 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
   },
   {
     description: "Field changed locally, deleted remotely",
     parent: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 12,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     }],
     remote: {
       "guid": "fff9fa27fa18",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
     },
     forked: {
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "cc-exp-month": 3,
     },
@@ -843,28 +843,28 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
   },
   {
     // Created, last modified should be synced; last used and times used should
     // be local. Remote created time older than local, remote modified time
     // newer than local.
     description: "Created, last modified time reconciliation without local changes",
     parent: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [],
     remote: {
       "guid": "5113f329c42f",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1200,
       "timeLastModified": 5700,
       "timeLastUsed": 5700,
       "timesUsed": 3,
     },
     reconciled: {
@@ -878,31 +878,31 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
     },
   },
   {
     // Local changes, remote created time newer than local, remote modified time
     // older than local.
     description: "Created, last modified time reconciliation with local changes",
     parent: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1234,
       "timeLastModified": 5678,
       "timeLastUsed": 5678,
       "timesUsed": 6,
     },
     local: [{
       "cc-name": "John Doe",
       "cc-number": "4929001587121045",
     }],
     remote: {
       "guid": "791e5608b80a",
-      "version": 1,
+      "version": 2,
       "cc-name": "John Doe",
       "cc-number": "4111111111111111",
       "timeCreated": 1300,
       "timeLastModified": 5000,
       "timeLastUsed": 5000,
       "timesUsed": 3,
     },
     reconciled: {
@@ -917,17 +917,17 @@ const CREDIT_CARD_RECONCILE_TESTCASES = 
 ];
 
 add_task(async function test_reconcile_unknown_version() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
 
   // Cross-version reconciliation isn't supported yet. See bug 1377204.
   await Assert.rejects(profileStorage.addresses.reconcile({
     "guid": "31d83d2725ec",
-    "version": 2,
+    "version": 3,
     "given-name": "Mark",
     "family-name": "Hammond",
   }), /Got unknown record version/);
 });
 
 add_task(async function test_reconcile_idempotent() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);