Bug 1486954 - Part I, Encrypt credit card numbers with OS key store. r=MattN
This patch morphs MasterPassword.jsm to OSKeyStore.jsm while keeping the same
API, as an adaptor between the API and the native API exposed as nsIOSKeyStore.idl.
Noted that OS Key Store has the concept of "recovery phrase" that we won't
be adopting here. The recovery phrase, together with our label, allow
the user to re-create the same key in OS key store.
Test case changes are needed because we have started asking for login in
places where we'll only do previously when "master password is enabled".
This also made some "when master password is enabled" tests invalid because
it is always considered enabled.
Some more test changes are needed simply because they previously rely on the
stable order of microtask resolutions (and the stable # of promises for a
specific operation). That has certainly changed with OSKeyStore.
The credit card form autofill is only enabled on Nightly.
Differential Revision: https://phabricator.services.mozilla.com/D4498

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"use strict";varEXPORTED_SYMBOLS=["CreditCard"];ChromeUtils.defineModuleGetter(this,"OSKeyStore","resource://formautofill/OSKeyStore.jsm");// The list of known and supported credit card network ids ("types")// This list mirrors the networks from dom/payments/BasicCardPayment.cpp// and is defined by https://www.w3.org/Payments/card-network-idsconstSUPPORTED_NETWORKS=Object.freeze(["amex","cartebancaire","diners","discover","jcb","mastercard","mir","unionpay","visa",]);classCreditCard{/** * @param {string} name * @param {string} number * @param {string} expirationString * @param {string|number} expirationMonth * @param {string|number} expirationYear * @param {string} network * @param {string|number} ccv * @param {string} encryptedNumber */constructor({name,number,expirationString,expirationMonth,expirationYear,network,ccv,encryptedNumber,}){this._name=name;this._unmodifiedNumber=number;this._encryptedNumber=encryptedNumber;this._ccv=ccv;this.number=number;// Only prefer the string version if missing one or both parsed formats.if(expirationString&&(!expirationMonth||!expirationYear)){this.expirationString=expirationString;}else{this.expirationMonth=expirationMonth;this.expirationYear=expirationYear;}if(network){this.network=network;}}setname(value){this._name=value;}setexpirationMonth(value){if(typeofvalue=="undefined"){this._expirationMonth=undefined;return;}this._expirationMonth=this._normalizeExpirationMonth(value);}getexpirationMonth(){returnthis._expirationMonth;}setexpirationYear(value){if(typeofvalue=="undefined"){this._expirationYear=undefined;return;}this._expirationYear=this._normalizeExpirationYear(value);}getexpirationYear(){returnthis._expirationYear;}setexpirationString(value){let{month,year}=this._parseExpirationString(value);this.expirationMonth=month;this.expirationYear=year;}setccv(value){this._ccv=value;}getnumber(){returnthis._number;}setnumber(value){if(value){letnormalizedNumber=value.replace(/[-\s]/g,"");// Based on the information on wiki[1], the shortest valid length should be// 12 digits (Maestro).// [1] https://en.wikipedia.org/wiki/Payment_card_numbernormalizedNumber=normalizedNumber.match(/^\d{12,}$/)?normalizedNumber:null;this._number=normalizedNumber;}}getnetwork(){returnthis._network;}setnetwork(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_numberisValidNumber(){if(!this._number){returnfalse;}// Remove dashes and whitespaceletnumber=this._number.replace(/[\-\s]/g,"");letlen=number.length;if(len<12||len>19){returnfalse;}if(!/^\d+$/.test(number)){returnfalse;}lettotal=0;for(leti=0;i<len;i++){letch=parseInt(number[len-i-1],10);if(i%2==1){// Double it, add digits together if > 10ch*=2;if(ch>9){ch-=9;}}total+=ch;}returntotal%10==0;}/** * Returns true if the card number is valid and the * expiration date has not passed. Otherwise false. * * @returns {boolean} */isValid(){if(!this.isValidNumber()){returnfalse;}letcurrentDate=newDate();letcurrentYear=currentDate.getFullYear();if(this._expirationYear>currentYear){returntrue;}// getMonth is 0-based, so add 1 because credit cards are 1-basedletcurrentMonth=currentDate.getMonth()+1;returnthis._expirationYear==currentYear&&this._expirationMonth>=currentMonth;}getmaskedNumber(){if(!this.isValidNumber()){thrownewError("Invalid credit card number");}return"*".repeat(4)+" "+this._number.substr(-4);}getlongMaskedNumber(){if(!this.isValidNumber()){thrownewError("Invalid credit card number");}return"*".repeat(this.number.length-4)+this.number.substr(-4);}/** * Get credit card display label. It should display masked numbers and the * cardholder's name, separated by a comma. If `showNumbers` is set to * true, decrypted credit card numbers are shown instead. */asyncgetLabel({showNumbers}={}){letparts=[];letlabel;if(showNumbers){if(this._encryptedNumber){label=awaitOSKeyStore.decrypt(this._encryptedNumber);}else{label=this._number;}}if(this._unmodifiedNumber&&!label){if(this.isValidNumber()){label=this.maskedNumber;}else{letmaskedNumber=CreditCard.formatMaskedNumber(this._unmodifiedNumber);label=`${maskedNumber.affix}${maskedNumber.label}`;}}if(label){parts.push(label);}if(this._name){parts.push(this._name);}returnparts.join(", ");}_normalizeExpirationMonth(month){month=parseInt(month,10);if(isNaN(month)||month<1||month>12){returnundefined;}returnmonth;}_normalizeExpirationYear(year){year=parseInt(year,10);if(isNaN(year)||year<0){returnundefined;}if(year<100){year+=2000;}returnyear;}_parseExpirationString(expirationString){letrules=[{regex:"(\\d{4})[-/](\\d{1,2})",yearIndex:1,monthIndex:2,},{regex:"(\\d{1,2})[-/](\\d{4})",yearIndex:2,monthIndex:1,},{regex:"(\\d{1,2})[-/](\\d{1,2})",},{regex:"(\\d{2})(\\d{2})",},];for(letruleofrules){letresult=newRegExp(`(?:^|\\D)${rule.regex}(?!\\d)`).exec(expirationString);if(!result){continue;}letyear,month;if(!rule.yearIndex||!rule.monthIndex){month=parseInt(result[1],10);if(month>12){year=parseInt(result[1],10);month=parseInt(result[2],10);}else{year=parseInt(result[2],10);}}else{year=parseInt(result[rule.yearIndex],10);month=parseInt(result[rule.monthIndex],10);}if((month<1||month>12)||(year>=100&&year<2000)){continue;}return{month,year};}return{month:undefined,year:undefined};}staticformatMaskedNumber(maskedNumber){return{affix:"****",label:maskedNumber.replace(/^\**/,""),};}staticgetMaskedNumber(number){letcreditCard=newCreditCard({number});returncreditCard.maskedNumber;}staticgetLongMaskedNumber(number){letcreditCard=newCreditCard({number});returncreditCard.longMaskedNumber;}staticisValidNumber(number){letcreditCard=newCreditCard({number});returncreditCard.isValidNumber();}staticisValidNetwork(network){returnSUPPORTED_NETWORKS.includes(network);}}CreditCard.SUPPORTED_NETWORKS=SUPPORTED_NETWORKS;