From 074c9a4b676310ca608e1a9d6c48d89307fa760c Mon Sep 17 00:00:00 2001 From: duchm Date: Fri, 23 Feb 2024 10:48:48 +0700 Subject: [PATCH] feat: 1pux import --- .../onepassword1PuxImporter.ts | 272 ++++++++++++++++++ src/services/import.service.ts | 4 + 2 files changed, 276 insertions(+) create mode 100644 src/importers/onepasswordImporters/onepassword1PuxImporter.ts diff --git a/src/importers/onepasswordImporters/onepassword1PuxImporter.ts b/src/importers/onepasswordImporters/onepassword1PuxImporter.ts new file mode 100644 index 0000000..a4877bd --- /dev/null +++ b/src/importers/onepasswordImporters/onepassword1PuxImporter.ts @@ -0,0 +1,272 @@ +import { BaseImporter } from '../baseImporter' +import { Importer } from '../importer' + +import { ImportResult } from '../../models/domain/importResult' + +import { CardView } from '../../models/view/cardView' +import { CipherView } from '../../models/view/cipherView' +import { IdentityView } from '../../models/view/identityView' +import { SecureNoteView } from '../../models/view/secureNoteView' + +import { CipherType } from '../../enums/cipherType' +import { FieldType } from '../../enums/fieldType' +import { SecureNoteType } from '../../enums/secureNoteType' +import { LoginView } from '../../models/view' + +enum Category { + Password = '001', + Identity = '004', + Note = '003', + CryptoWallet = '115', + Card = '002', + + // Nope + BankAccount = '101', + Membership = '105' +} + +// Only care about important data, extract the file to see full data +type DataFormat = { + accounts: { + attrs: { + name: string + } + vaults: { + attrs: { + name: string + } + items: { + categoryUuid: Category | string + details: { + loginFields: { + value: string + name: string + fieldType: 'T' | 'P' + designation: 'username' | 'password' + }[] + sections: { + title: string + name: string + fields: { + title: string + id: 'firstname' | 'initial' | 'lastname' + | 'gender' | 'birthdate' | 'occupation' | 'company' + | 'department' | 'jobtitle' | 'address' | 'defphone' + | 'homephone' | 'cellphone' | 'busphone' | 'email' | 'phone' + | 'member_name' | 'recoveryPhrase' | 'password' | 'walletAddress' + | 'bankName' | 'owner' | 'accountNo' | 'cardholder' | 'ccnum' | 'cvv' + | 'expiry' | 'validFrom' | 'bank' | 'pin' | string + value: { + string?: 'Fe' | 'Ma' | string + date?: number + address?: { + street: string + city: string + country: string + zip: string + state: string + } + phone?: string + url?: string + monthYear?: number // MMYYYY + concealed?: string + menu?: string + creditCardType?: string + creditCardNumber?: string + } + }[] + }[] + notesPlain?: string + } + overview: { + title: string + subtitle: string + url: string + tags?: string[] + } + }[] + }[] + }[] +} + +export class OnePassword1PuxImporter extends BaseImporter implements Importer { + parse(data: string): Promise { + const result = new ImportResult() + + try { + const parsed: DataFormat = JSON.parse(data) + parsed.accounts.forEach(account => { + account.vaults.forEach(vault => { + vault.items.forEach(item => { + this.processFolder(result, `${account.attrs.name}-${vault.attrs.name}`) + + // Basic info + const cipher = this.initLoginCipher() + cipher.name = item.overview.title + cipher.notes = item.details.notesPlain + this.processKvp(cipher, 'subtitle', item.overview.subtitle) + this.processKvp(cipher, 'tags', item.overview.tags?.join(', ')) + + // Parse by category + switch (item.categoryUuid) { + case Category.Password: { + this.processLoginFields(cipher, item.details.loginFields) + this.processSections(cipher, item.details.sections) + if (cipher.login) { + cipher.login.uris = this.makeUriArray(item.overview.url) + } + break + } + + case Category.Identity: { + const usedKeyIds = ['firstname', 'lastname', 'initial', 'address', 'company', 'email', 'defphone'] + cipher.type = CipherType.Identity + cipher.identity = new IdentityView() + cipher.identity.firstName = this.getValueByKeyId('firstname', item.details.sections)?.string + cipher.identity.lastName = this.getValueByKeyId('lastname', item.details.sections)?.string + cipher.identity.middleName = this.getValueByKeyId('initial', item.details.sections)?.string + const address = this.getValueByKeyId('address', item.details.sections)?.address + if (address) { + cipher.identity.state = address.state + cipher.identity.city = address.city + cipher.identity.country = address.country + cipher.identity.postalCode = address.zip + cipher.identity.address1 = address.street + } + cipher.identity.company = this.getValueByKeyId('company', item.details.sections)?.string + cipher.identity.email = this.getValueByKeyId('email', item.details.sections)?.string + cipher.identity.licenseNumber = '' + cipher.identity.passportNumber = '' + cipher.identity.phone = this.getValueByKeyId('defphone', item.details.sections)?.phone + cipher.identity.ssn = '' + this.processSections(cipher, item.details.sections, usedKeyIds) + break + } + + case Category.CryptoWallet: { + const usedKeyIds = ['password', 'pin', 'walletAddress', 'recoveryPhrase'] + cipher.type = CipherType.CryptoWallet + const cryptoWallet = { + password: this.getValueByKeyId('password', item.details.sections)?.concealed, + pin: this.getValueByKeyId('pin', item.details.sections)?.concealed, + address: this.getValueByKeyId('walletAddress', item.details.sections)?.string, + seed: this.getValueByKeyId('recoveryPhrase', item.details.sections)?.concealed, + notes: cipher.notes + } + cipher.notes = JSON.stringify(cryptoWallet) + this.processSections(cipher, item.details.sections, usedKeyIds) + break + } + + case Category.Card: { + const usedKeyIds = ['cardholder', 'cvv', 'expiry', 'ccnum'] + cipher.type = CipherType.Card + cipher.card = new CardView() + cipher.card.cardholderName = this.getValueByKeyId('cardholder', item.details.sections)?.string + cipher.card.code = this.getValueByKeyId('cvv', item.details.sections)?.concealed + const expiryDate = this.getValueByKeyId('expiry', item.details.sections)?.monthYear?.toString() + if (expiryDate) { + cipher.card.expMonth = expiryDate.slice(0, 2) + cipher.card.expYear = expiryDate.slice(2) + } + cipher.card.number = this.getValueByKeyId('ccnum', item.details.sections)?.creditCardNumber + cipher.card.brand = this.getCardBrand(cipher.card.number) + this.processSections(cipher, item.details.sections, usedKeyIds) + break + } + + default: { + if (item.details.loginFields?.length) { + this.processLoginFields(cipher, item.details.loginFields) + } else { + cipher.type = CipherType.SecureNote + cipher.secureNote = new SecureNoteView() + cipher.secureNote.type = SecureNoteType.Generic + } + this.processSections(cipher, item.details.sections) + } + } + + this.convertToNoteIfNeeded(cipher) + this.cleanupCipher(cipher) + result.ciphers.push(cipher) + }) + }) + }) + result.success = true + } catch (error) { + console.error('1pux importer', error) + } + + return Promise.resolve(result) + } + + private processLoginFields(cipher: CipherView, loginFields: DataFormat['accounts'][0]['vaults'][0]['items'][0]['details']['loginFields']) { + cipher.login = new LoginView() + loginFields.forEach(field => { + if (field.designation === 'username') { + cipher.login.username = field.value + } else if (field.designation === 'password') { + cipher.login.password = field.value + } else { + this.processKvp(cipher, field.name, field.value, field.fieldType === 'P' ? FieldType.Hidden : FieldType.Text) + } + }) + } + + private processSections( + cipher: CipherView, + sections: DataFormat['accounts'][0]['vaults'][0]['items'][0]['details']['sections'], + ignoredKeyIds: string[] = [] + ) { + sections.forEach(section => { + section.fields.forEach(field => { + if (ignoredKeyIds.includes(field.id)) { + return + } + let value: string = field.value.string + || field.value.phone + || field.value.url + || field.value.menu + || field.value.creditCardType + || field.value.creditCardNumber + || field.value.concealed + let type = FieldType.Text + if (field.value.date) { + const date = new Date(field.value.date * 1000) + const day = ('0' + date.getDate()).slice(-2) + const month = ('0' + (date.getMonth() + 1)).slice(-2) + const year = date.getFullYear(); + value = `${day}/${month}/${year}` + } + if (field.value.address) { + Object.keys(field.value.address).forEach(key => { + this.processKvp(cipher, key, field.value.address[key]) + }) + return + } + if (field.value.monthYear) { + value = `${field.value.monthYear.toString().slice(0, 2)}/${field.value.monthYear.toString().slice(2)}` + } + if (field.value.concealed) { + type = FieldType.Hidden + } + this.processKvp(cipher, field.title, value, type) + }) + }) + } + + private getValueByKeyId( + id: string, + sections: DataFormat['accounts'][0]['vaults'][0]['items'][0]['details']['sections'] + ) { + for (const section of sections) { + for (const field of section.fields) { + if (field.id === id) { + return field.value + } + } + } + return null + } +} diff --git a/src/services/import.service.ts b/src/services/import.service.ts index 1e26d46..babc7b5 100644 --- a/src/services/import.service.ts +++ b/src/services/import.service.ts @@ -57,6 +57,7 @@ import { MSecureCsvImporter } from '../importers/msecureCsvImporter' import { MykiCsvImporter } from '../importers/mykiCsvImporter' import { NordPassCsvImporter } from '../importers/nordpassCsvImporter' import { OnePassword1PifImporter } from '../importers/onepasswordImporters/onepassword1PifImporter' +import { OnePassword1PuxImporter } from '../importers/onepasswordImporters/onepassword1PuxImporter' import { OnePasswordMacCsvImporter } from '../importers/onepasswordImporters/onepasswordMacCsvImporter' import { OnePasswordWinCsvImporter } from '../importers/onepasswordImporters/onepasswordWinCsvImporter' import { PadlockCsvImporter } from '../importers/padlockCsvImporter' @@ -95,6 +96,7 @@ export class ImportService implements ImportServiceAbstraction { { id: 'firefoxcsv', name: 'Firefox (csv)' }, { id: 'safaricsv', name: 'Safari (csv)' }, { id: 'keepass2xml', name: 'KeePass 2 (xml)' }, + { id: '1password1pux', name: '1Password (1pux)' }, { id: '1password1pif', name: '1Password (1pif)' }, { id: 'dashlanecsv', name: 'Dashlane (csv)' }, { id: 'dashlanejson', name: 'Dashlane (json)' }, @@ -250,6 +252,8 @@ export class ImportService implements ImportServiceAbstraction { return new MeldiumCsvImporter() case '1password1pif': return new OnePassword1PifImporter() + case '1password1pux': + return new OnePassword1PuxImporter() case '1passwordwincsv': return new OnePasswordWinCsvImporter() case '1passwordmaccsv':