Bug 1485105 - Allow 12-19 digit length card numbers. r=MattN
authorSam Foster <sfoster@mozilla.com>
Tue, 16 Oct 2018 05:37:39 +0000
changeset 489752 583d689d630e68cb1f2ae5ae1e7f27f2ca28f359
parent 489751 b6f7a0b4345849cc9cf9a40abe1ff16cf1a6d905
child 489753 0329775bb9a4321441c576b01a6f851800afcf6e
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersMattN
bugs1485105
milestone64.0a1
Bug 1485105 - Allow 12-19 digit length card numbers. r=MattN * Change to isValidNumber to allow any number length in the range. This also removes 9 as a valid payment card number length * Amend form autocomplete test for sensitive 9 digit numbers. We no longer consider them valid cc numbers, test for 19 digit numbers instead * Fix intermittent issue in a session restore tests. It turns out Date.now().toString() can sometimes pass the Luhn algorithm and look like a valid credit card number. I believe this could lead to it being treated as sensitive data which is not saved and restored, failing the test Differential Revision: https://phabricator.services.mozilla.com/D8271
browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
browser/components/sessionstore/test/browser_463206.js
browser/components/sessionstore/test/browser_formdata_xpath.js
toolkit/components/satchel/test/test_form_submission.html
toolkit/modules/CreditCard.jsm
toolkit/modules/tests/xpcshell/test_CreditCard.js
--- a/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -5,17 +5,17 @@
 function test() {
   /** Test (B) for Bug 248970 **/
   waitForExplicitFinish();
 
   let windowsToClose = [];
   let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
   let filePath = file.path;
   let fieldList = {
-    "//input[@name='input']":     Date.now().toString(),
+    "//input[@name='input']":     Date.now().toString(16),
     "//input[@name='spaced 1']":  Math.random().toString(),
     "//input[3]":                 "three",
     "//input[@type='checkbox']":  true,
     "//input[@name='uncheck']":   false,
     "//input[@type='radio'][1]":  false,
     "//input[@type='radio'][2]":  true,
     "//input[@type='radio'][3]":  false,
     "//select":                   2,
--- a/browser/components/sessionstore/test/browser_463206.js
+++ b/browser/components/sessionstore/test/browser_463206.js
@@ -17,17 +17,17 @@ add_task(async function() {
     function typeText(aTextField, aValue) {
       aTextField.value = aValue;
 
       let event = aTextField.ownerDocument.createEvent("UIEvents");
       event.initUIEvent("input", true, true, aTextField.ownerGlobal, 0);
       aTextField.dispatchEvent(event);
     }
 
-    typeText(content.document.getElementById("out1"), Date.now());
+    typeText(content.document.getElementById("out1"), Date.now().toString(16));
     typeText(content.document.getElementsByName("1|#out2")[0], Math.random());
     typeText(content.frames[0].frames[1].document.getElementById("in1"), new Date());
   });
 
   // Duplicate the tab.
   let tab2 = gBrowser.duplicateTab(tab);
   await promiseTabRestored(tab2);
 
--- a/browser/components/sessionstore/test/browser_formdata_xpath.js
+++ b/browser/components/sessionstore/test/browser_formdata_xpath.js
@@ -16,17 +16,17 @@ add_task(function setup() {
     Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
   });
 });
 
 const FILE1 = createFilePath("346337_test1.file");
 const FILE2 = createFilePath("346337_test2.file");
 
 const FIELDS = {
-  "//input[@name='input']":     Date.now().toString(),
+  "//input[@name='input']":     Date.now().toString(16),
   "//input[@name='spaced 1']":  Math.random().toString(),
   "//input[3]":                 "three",
   "//input[@type='checkbox']":  true,
   "//input[@name='uncheck']":   false,
   "//input[@type='radio'][1]":  false,
   "//input[@type='radio'][2]":  true,
   "//input[@type='radio'][3]":  false,
   "//select":                   2,
--- a/toolkit/components/satchel/test/test_form_submission.html
+++ b/toolkit/components/satchel/test/test_form_submission.html
@@ -119,17 +119,17 @@
         input.type = "text";
         input.name = "test" + (i + 1);
         form.appendChild(input);
       }
     </script>
     <button type="submit">Submit</button>
   </form>
 
-  <!-- input with sensitive data (9 digit credit card number) -->
+  <!-- input with sensitive data (19 digit credit card number) -->
   <form id="form17" onsubmit="return checkSubmit(17)">
     <input type="text" name="test1">
     <button type="submit">Submit</button>
   </form>
 
   <!-- input with sensitive data (16 digit hyphenated credit card number) -->
   <form id="form18" onsubmit="return checkSubmit(18)">
     <input type="text" name="test1">
@@ -317,29 +317,29 @@ function startTest() {
   for (let i = 0; i != testData.length; i++) {
     $_(15, "test" + (i + 1)).value = testData[i];
   }
 
   testData = ccNumbers.valid15;
   for (let i = 0; i != testData.length; i++) {
     $_(16, "test" + (i + 1)).value = testData[i];
   }
-  $_(17, "test1").value = "001064088";
+  $_(17, "test1").value = "6799990100000000019";
   $_(18, "test1").value = "0000-0000-0080-4609";
   $_(19, "test1").value = "0000 0000 0222 331";
   $_(20, "test1").value = "dontSaveThis";
   $_(21, "test1").value = "dontSaveThis";
   $_(22, "searchbar-history").value = "dontSaveThis";
 
   $_(101, "test1").value = "savedValue";
   $_(102, "test2").value = "savedValue";
   $_(103, "test3").value = "savedValue";
   $_(104, "test4").value = " trimTrailingAndLeadingSpace ";
   $_(105, "test5").value = "\t trimTrailingAndLeadingWhitespace\t ";
-  $_(106, "test6").value = "00000000109181";
+  $_(106, "test6").value = "55555555555544445553"; // passes luhn but too long
 
   testData = ccNumbers.invalid16;
   for (let i = 0; i != testData.length; i++) {
     $_(107, "test7_" + (i + 1)).value = testData[i];
   }
 
   testData = ccNumbers.invalid15;
   for (let i = 0; i != testData.length; i++) {
@@ -407,17 +407,17 @@ function checkSubmit(formNum) {
       checkForSave("test4", "trimTrailingAndLeadingSpace",
                    "checking saved value is trimmed on both sides");
       break;
     case 105:
       checkForSave("test5", "trimTrailingAndLeadingWhitespace",
                    "checking saved value is trimmed on both sides");
       break;
     case 106:
-      checkForSave("test6", "00000000109181", "checking saved value");
+      checkForSave("test6", "55555555555544445553", "checking saved value");
       break;
     case 107:
       for (let i = 0; i != ccNumbers.invalid16.length; i++) {
         checkForSave("test7_" + (i + 1), ccNumbers.invalid16[i], "checking saved value");
       }
       break;
     case 108:
       for (let i = 0; i != ccNumbers.invalid15.length; i++) {
--- a/toolkit/modules/CreditCard.jsm
+++ b/toolkit/modules/CreditCard.jsm
@@ -103,44 +103,46 @@ class CreditCard {
   get number() {
     return this._number;
   }
 
   set number(value) {
     if (value) {
       let normalizedNumber = value.replace(/[-\s]/g, "");
       // Based on the information on wiki[1], the shortest valid length should be
-      // 9 digits (Canadian SIN).
-      // [1] https://en.wikipedia.org/wiki/Social_Insurance_Number
-      normalizedNumber = normalizedNumber.match(/^\d{9,}$/) ?
+      // 12 digits (Maestro).
+      // [1] https://en.wikipedia.org/wiki/Payment_card_number
+      normalizedNumber = normalizedNumber.match(/^\d{12,}$/) ?
         normalizedNumber : null;
       this._number = normalizedNumber;
     }
   }
 
   get network() {
     return this._network;
   }
 
   set network(value) {
     this._network = value || undefined;
   }
 
   // Implements the Luhn checksum algorithm as described at
   // http://wikipedia.org/wiki/Luhn_algorithm
+  // Number digit lengths vary with network, but should fall within 12-19 range. [2]
+  // More details at https://en.wikipedia.org/wiki/Payment_card_number
   isValidNumber() {
     if (!this._number) {
       return false;
     }
 
     // Remove dashes and whitespace
     let number = this._number.replace(/[\-\s]/g, "");
 
     let len = number.length;
-    if (len != 9 && len != 15 && len != 16) {
+    if (len < 12 || len > 19) {
       return false;
     }
 
     if (!/^\d+$/.test(number)) {
       return false;
     }
 
     let total = 0;
--- a/toolkit/modules/tests/xpcshell/test_CreditCard.js
+++ b/toolkit/modules/tests/xpcshell/test_CreditCard.js
@@ -10,30 +10,52 @@ add_task(function isValidNumber() {
     if (shouldPass) {
       ok(CreditCard.isValidNumber(number), `${number} should be considered valid`);
     } else {
       ok(!CreditCard.isValidNumber(number), `${number} should not be considered valid`);
     }
   }
 
   testValid("0000000000000000", true);
+
+  testValid("41111111112", false); // passes Luhn but too short
+  testValid("4111-1111-112", false); // passes Luhn but too short
+  testValid("55555555555544440018", false); // passes Luhn but too long
+  testValid("5555 5555 5555 4444 0018", false); // passes Luhn but too long
+
   testValid("4929001587121045", true);
   testValid("5103059495477870", true);
   testValid("6011029476355493", true);
   testValid("3589993783099582", true);
   testValid("5415425865751454", true);
-  if (CreditCard.isValidNumber("30190729470495")) {
-    ok(false, "todo: 14-digit numbers (Diners Club) aren't supported by isValidNumber yet");
-  }
-  if (CreditCard.isValidNumber("36333851788250")) {
-    ok(false, "todo: 14-digit numbers (Diners Club) aren't supported by isValidNumber yet");
-  }
-  if (CreditCard.isValidNumber("3532596776688495393")) {
-    ok(false, "todo: 19-digit numbers (JCB, Discover, Maestro) could have 16-19 digits");
-  }
+
+  testValid("378282246310005", true); // American Express test number
+  testValid("371449635398431", true); // American Express test number
+  testValid("378734493671000", true); // American Express Corporate test number
+  testValid("5610591081018250", true); // Australian BankCard test number
+  testValid("6759649826438453", true); // Maestro test number
+  testValid("6799990100000000019", true); // 19 digit Maestro test number
+  testValid("6799-9901-0000-0000019", true); // 19 digit Maestro test number
+  testValid("30569309025904", true); // 14 digit Diners Club test number
+  testValid("38520000023237", true); // 14 digit Diners Club test number
+  testValid("6011111111111117", true); // Discover test number
+  testValid("6011000990139424", true); // Discover test number
+  testValid("3530111333300000", true); // JCB test number
+  testValid("3566002020360505", true); // JCB test number
+  testValid("3532596776688495393", true); // 19-digit JCB number. JCB, Discover, Maestro could have 16-19 digits
+  testValid("3532 5967 7668 8495393", true); // 19-digit JCB number. JCB, Discover, Maestro could have 16-19 digits
+  testValid("5555555555554444", true); // MasterCard test number
+  testValid("5105105105105100", true); // MasterCard test number
+  testValid("2221000000000009", true); // 2-series MasterCard test number
+  testValid("4111111111111111", true); // Visa test number
+  testValid("4012888888881881", true); // Visa test number
+  testValid("4222222222222", true); // 13 digit Visa test number
+  testValid("4222 2222 22222", true); // 13 digit Visa test number
+  testValid("4035 5010 0000 0008", true); // Visadebit/Cartebancaire test number
+
   testValid("5038146897157463", true);
   testValid("4026313395502338", true);
   testValid("6387060366272981", true);
   testValid("474915027480942", true);
   testValid("924894781317325", true);
   testValid("714816113937185", true);
   testValid("790466087343106", true);
   testValid("474320195408363", true);
@@ -50,17 +72,18 @@ add_task(function isValidNumber() {
   testValid("4302068493801686", true);
   testValid("2721398408985465", true);
   testValid("6160334316984331", true);
   testValid("8643619970075142", true);
   testValid("0218246069710785", true);
   testValid("0000-0000-0080-4609", true);
   testValid("0000 0000 0222 331", true);
   testValid("344060747836806", true);
-  testValid("001064088", true);
+  testValid("001064088", false); // too short
+  testValid("00-10-64-088", false); // still too short
   testValid("4929001587121046", false);
   testValid("5103059495477876", false);
   testValid("6011029476355494", false);
   testValid("3589993783099581", false);
   testValid("5415425865751455", false);
   testValid("5038146897157462", false);
   testValid("4026313395502336", false);
   testValid("6387060366272980", false);
@@ -112,16 +135,17 @@ add_task(function test_maskNumber() {
   }
   testMask("0000000000000000", "**** 0000");
   testMask("4929001587121045", "**** 1045");
   testMask("5103059495477870", "**** 7870");
   testMask("6011029476355493", "**** 5493");
   testMask("3589993783099582", "**** 9582");
   testMask("5415425865751454", "**** 1454");
   testMask("344060747836806", "**** 6806");
+  testMask("6799990100000000019", "**** 0019");
   Assert.throws(() => (new CreditCard({number: "1234"})).maskedNumber,
     /Invalid credit card number/,
     "Four or less numbers should throw when retrieving the maskedNumber");
 });
 
 add_task(function test_longMaskedNumber() {
   function testMask(number, expected) {
     let card = new CreditCard({number});
@@ -130,16 +154,18 @@ add_task(function test_longMaskedNumber(
   }
   testMask("0000000000000000", "************0000");
   testMask("4929001587121045", "************1045");
   testMask("5103059495477870", "************7870");
   testMask("6011029476355493", "************5493");
   testMask("3589993783099582", "************9582");
   testMask("5415425865751454", "************1454");
   testMask("344060747836806", "***********6806");
+  testMask("6799990100000000019", "***************0019");
+
   Assert.throws(() => (new CreditCard({number: "1234"})).longMaskedNumber,
     /Invalid credit card number/,
     "Four or less numbers should throw when retrieving the maskedNumber");
 });
 
 add_task(function test_isValid() {
   function testValid(number, expirationMonth, expirationYear, shouldPass, message) {
     let card = new CreditCard({