1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

Fix 1password importer (#217)

* Fix import of 1password csv

* 1password is using '\' as a quote escape character.

* 1password's csv headers are sometimes capitalized. We want to identify
them case insensitively

* Change cipher type based on csv type header

* Translate 1password data to correct fields

* Test identity and credit card import

* linter fixes

* Do not use node 'fs' module

Karma is being used for automated tests so node modules are not available

Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
This commit is contained in:
Matt Gibson
2020-12-04 12:29:31 -06:00
committed by GitHub
parent c9df039fa9
commit 6fb0646481
5 changed files with 143 additions and 19 deletions

View File

@@ -65,19 +65,21 @@ export abstract class BaseImporter {
'ort', 'adresse',
];
protected parseCsvOptions = {
encoding: 'UTF-8',
skipEmptyLines: false,
}
protected parseXml(data: string): Document {
const parser = new DOMParser();
const doc = parser.parseFromString(data, 'application/xml');
return doc != null && doc.querySelector('parsererror') == null ? doc : null;
}
protected parseCsv(data: string, header: boolean): any[] {
protected parseCsv(data: string, header: boolean, options: any = {}): any[] {
const parseOptions = Object.assign({ header: header }, this.parseCsvOptions, options);
data = this.splitNewLine(data).join('\n').trim();
const result = papa.parse(data, {
header: header,
encoding: 'UTF-8',
skipEmptyLines: false,
});
const result = papa.parse(data, parseOptions);
if (result.errors != null && result.errors.length > 0) {
result.errors.forEach((e) => {
if (e.row != null) {

View File

@@ -4,14 +4,17 @@ import { Importer } from './importer';
import { ImportResult } from '../models/domain/importResult';
import { CipherType } from '../enums/cipherType';
import { CardView } from '../models/view';
import { CardView, IdentityView } from '../models/view';
const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid'];
const IgnoredProperties = ['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes'];
export class OnePasswordWinCsvImporter extends BaseImporter implements Importer {
parse(data: string): ImportResult {
const result = new ImportResult();
const results = this.parseCsv(data, true);
const results = this.parseCsv(data, true, {
quoteChar: '"',
escapeChar: '\\',
});
if (results == null) {
result.success = false;
return result;
@@ -24,7 +27,29 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer
const cipher = this.initLoginCipher();
cipher.name = this.getValueOrDefault(this.getProp(value, 'title'), '--');
cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n';
cipher.notes = this.getValueOrDefault(this.getProp(value, 'notesPlain'), '') + '\n' +
this.getValueOrDefault(this.getProp(value, 'notes'), '') + '\n';
cipher.notes.trim();
const onePassType = this.getValueOrDefault(this.getProp(value, 'type'), 'Login')
switch (onePassType) {
case 'Credit Card':
cipher.type = CipherType.Card;
cipher.card = new CardView();
IgnoredProperties.push('type');
break;
case 'Identity':
cipher.type = CipherType.Identity;
cipher.identity = new IdentityView();
IgnoredProperties.push('type');
break;
case 'Login':
case 'Secure Note':
IgnoredProperties.push('type');
default:
break;
}
if (!this.isNullOrWhitespace(this.getProp(value, 'number')) &&
!this.isNullOrWhitespace(this.getProp(value, 'expiry date'))) {
@@ -50,30 +75,59 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer
const urls = value[property].split(this.newLineRegex);
cipher.login.uris = this.makeUriArray(urls);
continue;
} else if ((lowerProp === 'url')) {
if (cipher.login.uris == null) {
cipher.login.uris = [];
}
cipher.login.uris.concat(this.makeUriArray(value[property]));
continue;
}
} else if (cipher.type === CipherType.Card) {
if (this.isNullOrWhitespace(cipher.card.number) && lowerProp === 'number') {
if (this.isNullOrWhitespace(cipher.card.number) && lowerProp.includes('number')) {
cipher.card.number = value[property];
cipher.card.brand = this.getCardBrand(this.getProp(value, 'number'));
continue;
} else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp === 'verification number') {
} else if (this.isNullOrWhitespace(cipher.card.code) && lowerProp.includes('verification number')) {
cipher.card.code = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp === 'cardholder name') {
} else if (this.isNullOrWhitespace(cipher.card.cardholderName) && lowerProp.includes('cardholder name')) {
cipher.card.cardholderName = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp === 'expiry date' &&
value[property].length === 6) {
cipher.card.expMonth = (value[property] as string).substr(4, 2);
} else if (this.isNullOrWhitespace(cipher.card.expiration) && lowerProp.includes('expiry date') &&
value[property].length === 7) {
cipher.card.expMonth = (value[property] as string).substr(0, 2);
if (cipher.card.expMonth[0] === '0') {
cipher.card.expMonth = cipher.card.expMonth.substr(1, 1);
}
cipher.card.expYear = (value[property] as string).substr(0, 4);
cipher.card.expYear = (value[property] as string).substr(3, 4);
continue;
} else if (lowerProp === 'type') {
} else if (lowerProp === 'type' || lowerProp === 'type(type)') {
// Skip since brand was determined from number above
continue;
}
} else if (cipher.type === CipherType.Identity) {
if (this.isNullOrWhitespace(cipher.identity.firstName) && lowerProp.includes('first name')) {
cipher.identity.firstName = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.middleName) && lowerProp.includes('initial')) {
cipher.identity.middleName = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.lastName) && lowerProp.includes('last name')) {
cipher.identity.lastName = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.username) && lowerProp.includes('username')) {
cipher.identity.username = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.company) && lowerProp.includes('company')) {
cipher.identity.company = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.phone) && lowerProp.includes('default phone')) {
cipher.identity.phone = value[property];
continue;
} else if (this.isNullOrWhitespace(cipher.identity.email) && lowerProp.includes('email')) {
cipher.identity.email = value[property];
continue;
}
}
if (IgnoredProperties.indexOf(lowerProp) === -1 && !lowerProp.startsWith('section:') &&
@@ -81,6 +135,11 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer
if (altUsername == null && lowerProp === 'email') {
altUsername = value[property];
}
else if (lowerProp === 'created date' || lowerProp === 'modified date') {
const readableDate = new Date(parseInt(value[property], 10) * 1000).toUTCString();
this.processKvp(cipher, '1Password ' + property, readableDate);
continue;
}
this.processKvp(cipher, property, value[property]);
}
}
@@ -100,6 +159,10 @@ export class OnePasswordWinCsvImporter extends BaseImporter implements Importer
}
private getProp(obj: any, name: string): any {
return obj[name] || obj[name.toUpperCase()];
const lowerObj = Object.entries(obj).reduce((agg: any, entry: [string, any]) => {
agg[entry[0].toLowerCase()] = entry[1];
return agg;
}, {});
return lowerObj[name.toLowerCase()];
}
}