diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index 9deda95d36..1373669ca1 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -163,6 +163,7 @@ class="monospaced" type="{{ showCardNumber ? 'text' : 'password' }}" name="Card.Number" + (input)="onCardNumberChange()" [(ngModel)]="cipher.card.number" appInputVerbatim [readonly]="!cipher.edit && editMode" diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index c8595c816e..c26078394a 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -148,6 +148,7 @@ class="monospaced" type="{{ showCardNumber ? 'text' : 'password' }}" name="Card.Number" + (input)="onCardNumberChange()" [(ngModel)]="cipher.card.number" appInputVerbatim [readonly]="!cipher.edit && editMode" diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index f551d8e752..a23332a2f9 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -440,6 +440,7 @@ class="form-control text-monospace" type="{{ showCardNumber ? 'text' : 'password' }}" name="Card.Number" + (input)="onCardNumberChange()" [(ngModel)]="cipher.card.number" appInputVerbatim autocomplete="new-password" diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 19896c20c1..83b8e3e0fd 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -368,6 +368,10 @@ export class AddEditComponent implements OnInit, OnDestroy { } } + onCardNumberChange(): void { + this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number); + } + getCardExpMonthDisplay() { return this.cardExpMonthOptions.find((x) => x.value == this.cipher.card.expMonth)?.name; } diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index abeb381a7a..25c7097707 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -81,4 +81,67 @@ export class CardView extends ItemView { static fromJSON(obj: Partial>): CardView { return Object.assign(new CardView(), obj); } + + // ref https://stackoverflow.com/a/5911300 + static getCardBrandByPatterns(cardNum: string): string { + if (cardNum == null || typeof cardNum !== "string" || cardNum.trim() === "") { + return null; + } + + // Visa + let re = new RegExp("^4"); + if (cardNum.match(re) != null) { + return "Visa"; + } + + // Mastercard + // Updated for Mastercard 2017 BINs expansion + if ( + /^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test( + cardNum + ) + ) { + return "Mastercard"; + } + + // AMEX + re = new RegExp("^3[47]"); + if (cardNum.match(re) != null) { + return "Amex"; + } + + // Discover + re = new RegExp( + "^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)" + ); + if (cardNum.match(re) != null) { + return "Discover"; + } + + // Diners + re = new RegExp("^36"); + if (cardNum.match(re) != null) { + return "Diners Club"; + } + + // Diners - Carte Blanche + re = new RegExp("^30[0-5]"); + if (cardNum.match(re) != null) { + return "Diners Club"; + } + + // JCB + re = new RegExp("^35(2[89]|[3-8][0-9])"); + if (cardNum.match(re) != null) { + return "JCB"; + } + + // Visa Electron + re = new RegExp("^(4026|417500|4508|4844|491(3|7))"); + if (cardNum.match(re) != null) { + return "Visa"; + } + + return null; + } } diff --git a/libs/importer/src/importers/avast/avast-json-importer.ts b/libs/importer/src/importers/avast/avast-json-importer.ts index 0859603230..92d407e945 100644 --- a/libs/importer/src/importers/avast/avast-json-importer.ts +++ b/libs/importer/src/importers/avast/avast-json-importer.ts @@ -1,5 +1,6 @@ import { SecureNoteType } from "@bitwarden/common/enums"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { ImportResult } from "../../models/import-result"; import { BaseImporter } from "../base-importer"; @@ -48,7 +49,7 @@ export class AvastJsonImporter extends BaseImporter implements Importer { cipher.card.cardholderName = this.getValueOrDefault(value.holderName); cipher.card.number = this.getValueOrDefault(value.cardNumber); cipher.card.code = this.getValueOrDefault(value.cvv); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); if (value.expirationDate != null) { if (value.expirationDate.month != null) { cipher.card.expMonth = value.expirationDate.month + ""; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index d18f1d49e3..fed35a391a 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -241,69 +241,6 @@ export abstract class BaseImporter { return str.split(this.newLineRegex); } - // ref https://stackoverflow.com/a/5911300 - protected getCardBrand(cardNum: string) { - if (this.isNullOrWhitespace(cardNum)) { - return null; - } - - // Visa - let re = new RegExp("^4"); - if (cardNum.match(re) != null) { - return "Visa"; - } - - // Mastercard - // Updated for Mastercard 2017 BINs expansion - if ( - /^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$/.test( - cardNum - ) - ) { - return "Mastercard"; - } - - // AMEX - re = new RegExp("^3[47]"); - if (cardNum.match(re) != null) { - return "Amex"; - } - - // Discover - re = new RegExp( - "^(6011|622(12[6-9]|1[3-9][0-9]|[2-8][0-9]{2}|9[0-1][0-9]|92[0-5]|64[4-9])|65)" - ); - if (cardNum.match(re) != null) { - return "Discover"; - } - - // Diners - re = new RegExp("^36"); - if (cardNum.match(re) != null) { - return "Diners Club"; - } - - // Diners - Carte Blanche - re = new RegExp("^30[0-5]"); - if (cardNum.match(re) != null) { - return "Diners Club"; - } - - // JCB - re = new RegExp("^35(2[89]|[3-8][0-9])"); - if (cardNum.match(re) != null) { - return "JCB"; - } - - // Visa Electron - re = new RegExp("^(4026|417500|4508|4844|491(3|7))"); - if (cardNum.match(re) != null) { - return "Visa"; - } - - return null; - } - protected setCardExpiration(cipher: CipherView, expiration: string): boolean { if (this.isNullOrWhitespace(expiration)) { return false; diff --git a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts index 4a16f9dc99..45aff447e3 100644 --- a/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts +++ b/libs/importer/src/importers/dashlane/dashlane-csv-importer.ts @@ -136,7 +136,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer { case "credit_card": cipher.card.cardholderName = row.account_name; cipher.card.number = row.cc_number; - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.code = row.code; this.setCardExpiration(cipher, `${row.expiration_month}/${row.expiration_year}`); diff --git a/libs/importer/src/importers/dashlane/dashlane-json-importer.ts b/libs/importer/src/importers/dashlane/dashlane-json-importer.ts index 1037df096b..2cfb29f21a 100644 --- a/libs/importer/src/importers/dashlane/dashlane-json-importer.ts +++ b/libs/importer/src/importers/dashlane/dashlane-json-importer.ts @@ -134,7 +134,7 @@ export class DashlaneJsonImporter extends BaseImporter implements Importer { cipher.type = CipherType.Card; cipher.name = this.getValueOrDefault(obj.bank); cipher.card.number = this.getValueOrDefault(obj.cardNumber); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.cardholderName = this.getValueOrDefault(obj.owner); if (!this.isNullOrWhitespace(cipher.card.brand)) { if (this.isNullOrWhitespace(cipher.name)) { diff --git a/libs/importer/src/importers/encryptr-csv-importer.ts b/libs/importer/src/importers/encryptr-csv-importer.ts index 73796cf1e3..a925ad2439 100644 --- a/libs/importer/src/importers/encryptr-csv-importer.ts +++ b/libs/importer/src/importers/encryptr-csv-importer.ts @@ -38,7 +38,7 @@ export class EncryptrCsvImporter extends BaseImporter implements Importer { cipher.card = new CardView(); cipher.card.cardholderName = this.getValueOrDefault(value["Name on card"]); cipher.card.number = this.getValueOrDefault(value["Card Number"]); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.code = this.getValueOrDefault(value.CVV); const expiry = this.getValueOrDefault(value.Expiry); if (!this.isNullOrWhitespace(expiry)) { diff --git a/libs/importer/src/importers/enpass/enpass-csv-importer.ts b/libs/importer/src/importers/enpass/enpass-csv-importer.ts index 26611b182e..546dd33b22 100644 --- a/libs/importer/src/importers/enpass/enpass-csv-importer.ts +++ b/libs/importer/src/importers/enpass/enpass-csv-importer.ts @@ -90,7 +90,7 @@ export class EnpassCsvImporter extends BaseImporter implements Importer { continue; } else if (fieldNameLower === "number" && this.isNullOrWhitespace(cipher.card.number)) { cipher.card.number = fieldValue; - cipher.card.brand = this.getCardBrand(fieldValue); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); continue; } else if (fieldNameLower === "cvc" && this.isNullOrWhitespace(cipher.card.code)) { cipher.card.code = fieldValue; diff --git a/libs/importer/src/importers/enpass/enpass-json-importer.ts b/libs/importer/src/importers/enpass/enpass-json-importer.ts index 0af291e0ca..76306721e5 100644 --- a/libs/importer/src/importers/enpass/enpass-json-importer.ts +++ b/libs/importer/src/importers/enpass/enpass-json-importer.ts @@ -126,7 +126,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer { cipher.card.cardholderName = field.value; } else if (field.type === "ccNumber" && this.isNullOrWhitespace(cipher.card.number)) { cipher.card.number = field.value; - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); } else if (field.type === "ccCvc" && this.isNullOrWhitespace(cipher.card.code)) { cipher.card.code = field.value; } else if (field.type === "ccExpiry" && this.isNullOrWhitespace(cipher.card.expYear)) { diff --git a/libs/importer/src/importers/fsecure/fsecure-fsk-importer.ts b/libs/importer/src/importers/fsecure/fsecure-fsk-importer.ts index ba66aa8dad..db431a6456 100644 --- a/libs/importer/src/importers/fsecure/fsecure-fsk-importer.ts +++ b/libs/importer/src/importers/fsecure/fsecure-fsk-importer.ts @@ -66,7 +66,7 @@ export class FSecureFskImporter extends BaseImporter implements Importer { cipher.card = new CardView(); cipher.card.cardholderName = this.getValueOrDefault(entry.username); cipher.card.number = this.getValueOrDefault(entry.creditNumber); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.code = this.getValueOrDefault(entry.creditCvv); if (!this.isNullOrWhitespace(entry.creditExpiry)) { if (!this.setCardExpiration(cipher, entry.creditExpiry)) { diff --git a/libs/importer/src/importers/lastpass-csv-importer.ts b/libs/importer/src/importers/lastpass-csv-importer.ts index 53d46b831f..464b1d3429 100644 --- a/libs/importer/src/importers/lastpass-csv-importer.ts +++ b/libs/importer/src/importers/lastpass-csv-importer.ts @@ -122,7 +122,7 @@ export class LastPassCsvImporter extends BaseImporter implements Importer { card.cardholderName = this.getValueOrDefault(value.ccname); card.number = this.getValueOrDefault(value.ccnum); card.code = this.getValueOrDefault(value.cccsc); - card.brand = this.getCardBrand(value.ccnum); + card.brand = CardView.getCardBrandByPatterns(card.number); if (!this.isNullOrWhitespace(value.ccexp) && value.ccexp.indexOf("-") > -1) { const ccexpParts = (value.ccexp as string).split("-"); diff --git a/libs/importer/src/importers/myki-csv-importer.ts b/libs/importer/src/importers/myki-csv-importer.ts index 1b6fd9afdb..c75a409376 100644 --- a/libs/importer/src/importers/myki-csv-importer.ts +++ b/libs/importer/src/importers/myki-csv-importer.ts @@ -72,7 +72,7 @@ export class MykiCsvImporter extends BaseImporter implements Importer { cipher.type = CipherType.Card; cipher.card.cardholderName = this.getValueOrDefault(value.cardName); cipher.card.number = this.getValueOrDefault(value.cardNumber); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.expMonth = this.getValueOrDefault(value.exp_month); cipher.card.expYear = this.getValueOrDefault(value.exp_year); cipher.card.code = this.getValueOrDefault(value.cvv); diff --git a/libs/importer/src/importers/nordpass-csv-importer.ts b/libs/importer/src/importers/nordpass-csv-importer.ts index 7eb445335a..d17a9fbfdd 100644 --- a/libs/importer/src/importers/nordpass-csv-importer.ts +++ b/libs/importer/src/importers/nordpass-csv-importer.ts @@ -1,5 +1,6 @@ import { SecureNoteType } from "@bitwarden/common/enums"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; @@ -66,7 +67,7 @@ export class NordPassCsvImporter extends BaseImporter implements Importer { cipher.card.cardholderName = this.getValueOrDefault(record.cardholdername); cipher.card.number = this.getValueOrDefault(record.cardnumber); cipher.card.code = this.getValueOrDefault(record.cvc); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); this.setCardExpiration(cipher, record.expirydate); break; diff --git a/libs/importer/src/importers/onepassword/onepassword-1pif-importer.ts b/libs/importer/src/importers/onepassword/onepassword-1pif-importer.ts index 688adf3bd7..a4f78d203b 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pif-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pif-importer.ts @@ -198,7 +198,7 @@ export class OnePassword1PifImporter extends BaseImporter implements Importer { } else if (cipher.type === CipherType.Card) { if (this.isNullOrWhitespace(cipher.card.number) && fieldDesignation === "ccnum") { cipher.card.number = fieldValue; - cipher.card.brand = this.getCardBrand(fieldValue); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); return; } else if (this.isNullOrWhitespace(cipher.card.code) && fieldDesignation === "cvv") { cipher.card.code = fieldValue; diff --git a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts index ebf596a72e..b8329a5d44 100644 --- a/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-1pux-importer.ts @@ -407,7 +407,7 @@ export class OnePassword1PuxImporter extends BaseImporter implements Importer { private fillCreditCard(field: FieldsEntity, fieldValue: string, cipher: CipherView): boolean { if (this.isNullOrWhitespace(cipher.card.number) && field.id === "ccnum") { cipher.card.number = fieldValue; - cipher.card.brand = this.getCardBrand(fieldValue); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); return true; } diff --git a/libs/importer/src/importers/onepassword/onepassword-csv-importer.ts b/libs/importer/src/importers/onepassword/onepassword-csv-importer.ts index f756d61941..3f920a7d4f 100644 --- a/libs/importer/src/importers/onepassword/onepassword-csv-importer.ts +++ b/libs/importer/src/importers/onepassword/onepassword-csv-importer.ts @@ -1,5 +1,6 @@ import { FieldType } from "@bitwarden/common/enums"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ImportResult } from "../../models/import-result"; @@ -295,7 +296,7 @@ export abstract class OnePasswordCsvImporter extends BaseImporter implements Imp context.lowerProperty.includes("number") ) { context.cipher.card.number = context.importRecord[context.property]; - context.cipher.card.brand = this.getCardBrand(context.cipher.card.number); + context.cipher.card.brand = CardView.getCardBrandByPatterns(context.cipher.card.number); return true; } return false; diff --git a/libs/importer/src/importers/passwordboss-json-importer.ts b/libs/importer/src/importers/passwordboss-json-importer.ts index f819408d22..8c42657506 100644 --- a/libs/importer/src/importers/passwordboss-json-importer.ts +++ b/libs/importer/src/importers/passwordboss-json-importer.ts @@ -75,7 +75,7 @@ export class PasswordBossJsonImporter extends BaseImporter implements Importer { if (cipher.type === CipherType.Card) { if (property === "cardNumber") { cipher.card.number = val; - cipher.card.brand = this.getCardBrand(val); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); continue; } else if (property === "nameOnCard") { cipher.card.cardholderName = val; diff --git a/libs/importer/src/importers/remembear-csv-importer.ts b/libs/importer/src/importers/remembear-csv-importer.ts index 236245f230..f23271892d 100644 --- a/libs/importer/src/importers/remembear-csv-importer.ts +++ b/libs/importer/src/importers/remembear-csv-importer.ts @@ -31,7 +31,7 @@ export class RememBearCsvImporter extends BaseImporter implements Importer { cipher.card = new CardView(); cipher.card.cardholderName = this.getValueOrDefault(value.cardholder); cipher.card.number = this.getValueOrDefault(value.number); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); cipher.card.code = this.getValueOrDefault(value.verification); try { diff --git a/libs/importer/src/importers/truekey-csv-importer.ts b/libs/importer/src/importers/truekey-csv-importer.ts index f2352ac3a8..3d52be1bd8 100644 --- a/libs/importer/src/importers/truekey-csv-importer.ts +++ b/libs/importer/src/importers/truekey-csv-importer.ts @@ -50,7 +50,7 @@ export class TrueKeyCsvImporter extends BaseImporter implements Importer { cipher.card = new CardView(); cipher.card.cardholderName = this.getValueOrDefault(value.cardholder); cipher.card.number = this.getValueOrDefault(value.number); - cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.brand = CardView.getCardBrandByPatterns(cipher.card.number); if (!this.isNullOrWhitespace(value.expiryDate)) { try { const expDate = new Date(value.expiryDate);