feat: support Bitwarden extended cipher types

This commit is contained in:
shuaiplus
2026-07-06 00:44:17 +08:00
parent 01ff627ac6
commit 109593da90
18 changed files with 1019 additions and 11 deletions
+198
View File
@@ -513,6 +513,30 @@ async function encryptTextValue(value: string, enc: Uint8Array, mac: Uint8Array)
return encryptBw(new TextEncoder().encode(s), enc, mac);
}
function stripDecodedObjectFields(value: unknown): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
const out: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) {
if (/^dec[A-Z]/.test(key)) continue;
out[key] = item;
}
return out;
}
async function encryptObjectFields(
existing: unknown,
entries: Array<[string, string]>,
draft: VaultDraft,
enc: Uint8Array,
mac: Uint8Array
): Promise<Record<string, unknown>> {
const out = stripDecodedObjectFields(existing);
for (const [fieldName, draftKey] of entries) {
out[fieldName] = await encryptTextValue(String((draft as unknown as Record<string, unknown>)[draftKey] || ''), enc, mac);
}
return out;
}
async function encryptPasswordHistory(
entries: CipherPasswordHistoryEntry[] | null | undefined,
enc: Uint8Array,
@@ -587,6 +611,40 @@ function draftFromDecryptedCipher(cipher: Cipher): VaultDraft {
sshPrivateKey: '',
sshPublicKey: '',
sshFingerprint: '',
bankName: '',
bankNameOnAccount: '',
bankAccountType: '',
bankAccountNumber: '',
bankRoutingNumber: '',
bankBranchNumber: '',
bankPin: '',
bankSwiftCode: '',
bankIban: '',
bankContactPhone: '',
licenseFirstName: '',
licenseMiddleName: '',
licenseLastName: '',
licenseDateOfBirth: '',
licenseNumber: '',
licenseIssuingCountry: '',
licenseIssuingState: '',
licenseIssueDate: '',
licenseExpirationDate: '',
licenseIssuingAuthority: '',
licenseClass: '',
passportSurname: '',
passportGivenName: '',
passportDateOfBirth: '',
passportSex: '',
passportBirthPlace: '',
passportNationality: '',
passportIssuingCountry: '',
passportNumber: '',
passportType: '',
passportNationalIdentificationNumber: '',
passportIssuingAuthority: '',
passportIssueDate: '',
passportExpirationDate: '',
customFields: [],
};
@@ -662,6 +720,43 @@ function draftFromDecryptedCipher(cipher: Cipher): VaultDraft {
cipher.sshKey.decFingerprint,
cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint
);
} else if (type === 6 && cipher.bankAccount) {
draft.bankName = plainCipherValue(cipher.bankAccount.decBankName, cipher.bankAccount.bankName);
draft.bankNameOnAccount = plainCipherValue(cipher.bankAccount.decNameOnAccount, cipher.bankAccount.nameOnAccount);
draft.bankAccountType = plainCipherValue(cipher.bankAccount.decAccountType, cipher.bankAccount.accountType);
draft.bankAccountNumber = plainCipherValue(cipher.bankAccount.decAccountNumber, cipher.bankAccount.accountNumber);
draft.bankRoutingNumber = plainCipherValue(cipher.bankAccount.decRoutingNumber, cipher.bankAccount.routingNumber);
draft.bankBranchNumber = plainCipherValue(cipher.bankAccount.decBranchNumber, cipher.bankAccount.branchNumber);
draft.bankPin = plainCipherValue(cipher.bankAccount.decPin, cipher.bankAccount.pin);
draft.bankSwiftCode = plainCipherValue(cipher.bankAccount.decSwiftCode, cipher.bankAccount.swiftCode);
draft.bankIban = plainCipherValue(cipher.bankAccount.decIban, cipher.bankAccount.iban);
draft.bankContactPhone = plainCipherValue(cipher.bankAccount.decBankContactPhone, cipher.bankAccount.bankContactPhone);
} else if (type === 7 && cipher.driversLicense) {
draft.licenseFirstName = plainCipherValue(cipher.driversLicense.decFirstName, cipher.driversLicense.firstName);
draft.licenseMiddleName = plainCipherValue(cipher.driversLicense.decMiddleName, cipher.driversLicense.middleName);
draft.licenseLastName = plainCipherValue(cipher.driversLicense.decLastName, cipher.driversLicense.lastName);
draft.licenseDateOfBirth = plainCipherValue(cipher.driversLicense.decDateOfBirth, cipher.driversLicense.dateOfBirth);
draft.licenseNumber = plainCipherValue(cipher.driversLicense.decLicenseNumber, cipher.driversLicense.licenseNumber);
draft.licenseIssuingCountry = plainCipherValue(cipher.driversLicense.decIssuingCountry, cipher.driversLicense.issuingCountry);
draft.licenseIssuingState = plainCipherValue(cipher.driversLicense.decIssuingState, cipher.driversLicense.issuingState);
draft.licenseIssueDate = plainCipherValue(cipher.driversLicense.decIssueDate, cipher.driversLicense.issueDate);
draft.licenseExpirationDate = plainCipherValue(cipher.driversLicense.decExpirationDate, cipher.driversLicense.expirationDate);
draft.licenseIssuingAuthority = plainCipherValue(cipher.driversLicense.decIssuingAuthority, cipher.driversLicense.issuingAuthority);
draft.licenseClass = plainCipherValue(cipher.driversLicense.decLicenseClass, cipher.driversLicense.licenseClass);
} else if (type === 8 && cipher.passport) {
draft.passportSurname = plainCipherValue(cipher.passport.decSurname, cipher.passport.surname);
draft.passportGivenName = plainCipherValue(cipher.passport.decGivenName, cipher.passport.givenName);
draft.passportDateOfBirth = plainCipherValue(cipher.passport.decDateOfBirth, cipher.passport.dateOfBirth);
draft.passportSex = plainCipherValue(cipher.passport.decSex, cipher.passport.sex);
draft.passportBirthPlace = plainCipherValue(cipher.passport.decBirthPlace, cipher.passport.birthPlace);
draft.passportNationality = plainCipherValue(cipher.passport.decNationality, cipher.passport.nationality);
draft.passportIssuingCountry = plainCipherValue(cipher.passport.decIssuingCountry, cipher.passport.issuingCountry);
draft.passportNumber = plainCipherValue(cipher.passport.decPassportNumber, cipher.passport.passportNumber);
draft.passportType = plainCipherValue(cipher.passport.decPassportType, cipher.passport.passportType);
draft.passportNationalIdentificationNumber = plainCipherValue(cipher.passport.decNationalIdentificationNumber, cipher.passport.nationalIdentificationNumber);
draft.passportIssuingAuthority = plainCipherValue(cipher.passport.decIssuingAuthority, cipher.passport.issuingAuthority);
draft.passportIssueDate = plainCipherValue(cipher.passport.decIssueDate, cipher.passport.issueDate);
draft.passportExpirationDate = plainCipherValue(cipher.passport.decExpirationDate, cipher.passport.expirationDate);
}
return draft;
@@ -983,6 +1078,10 @@ function getCipherKeyMismatchProbes(cipher: Cipher): string[] {
cipher.identity?.title,
cipher.identity?.firstName,
cipher.sshKey?.privateKey,
cipher.bankAccount?.bankName,
cipher.bankAccount?.accountNumber,
cipher.driversLicense?.licenseNumber,
cipher.passport?.passportNumber,
...(cipher.fields || []).flatMap((field) => [field.name, field.value]),
];
const probes: string[] = [];
@@ -1053,6 +1152,40 @@ function hasUnresolvedEncryptedFields(cipher: Cipher): boolean {
[cipher.sshKey?.privateKey, cipher.sshKey?.decPrivateKey],
[cipher.sshKey?.publicKey, cipher.sshKey?.decPublicKey],
[cipher.sshKey?.keyFingerprint || cipher.sshKey?.fingerprint, cipher.sshKey?.decFingerprint],
[cipher.bankAccount?.bankName, cipher.bankAccount?.decBankName],
[cipher.bankAccount?.nameOnAccount, cipher.bankAccount?.decNameOnAccount],
[cipher.bankAccount?.accountType, cipher.bankAccount?.decAccountType],
[cipher.bankAccount?.accountNumber, cipher.bankAccount?.decAccountNumber],
[cipher.bankAccount?.routingNumber, cipher.bankAccount?.decRoutingNumber],
[cipher.bankAccount?.branchNumber, cipher.bankAccount?.decBranchNumber],
[cipher.bankAccount?.pin, cipher.bankAccount?.decPin],
[cipher.bankAccount?.swiftCode, cipher.bankAccount?.decSwiftCode],
[cipher.bankAccount?.iban, cipher.bankAccount?.decIban],
[cipher.bankAccount?.bankContactPhone, cipher.bankAccount?.decBankContactPhone],
[cipher.driversLicense?.firstName, cipher.driversLicense?.decFirstName],
[cipher.driversLicense?.middleName, cipher.driversLicense?.decMiddleName],
[cipher.driversLicense?.lastName, cipher.driversLicense?.decLastName],
[cipher.driversLicense?.dateOfBirth, cipher.driversLicense?.decDateOfBirth],
[cipher.driversLicense?.licenseNumber, cipher.driversLicense?.decLicenseNumber],
[cipher.driversLicense?.issuingCountry, cipher.driversLicense?.decIssuingCountry],
[cipher.driversLicense?.issuingState, cipher.driversLicense?.decIssuingState],
[cipher.driversLicense?.issueDate, cipher.driversLicense?.decIssueDate],
[cipher.driversLicense?.expirationDate, cipher.driversLicense?.decExpirationDate],
[cipher.driversLicense?.issuingAuthority, cipher.driversLicense?.decIssuingAuthority],
[cipher.driversLicense?.licenseClass, cipher.driversLicense?.decLicenseClass],
[cipher.passport?.surname, cipher.passport?.decSurname],
[cipher.passport?.givenName, cipher.passport?.decGivenName],
[cipher.passport?.dateOfBirth, cipher.passport?.decDateOfBirth],
[cipher.passport?.sex, cipher.passport?.decSex],
[cipher.passport?.birthPlace, cipher.passport?.decBirthPlace],
[cipher.passport?.nationality, cipher.passport?.decNationality],
[cipher.passport?.issuingCountry, cipher.passport?.decIssuingCountry],
[cipher.passport?.passportNumber, cipher.passport?.decPassportNumber],
[cipher.passport?.passportType, cipher.passport?.decPassportType],
[cipher.passport?.nationalIdentificationNumber, cipher.passport?.decNationalIdentificationNumber],
[cipher.passport?.issuingAuthority, cipher.passport?.decIssuingAuthority],
[cipher.passport?.issueDate, cipher.passport?.decIssueDate],
[cipher.passport?.expirationDate, cipher.passport?.decExpirationDate],
...(cipher.fields || []).flatMap((field) => [
[field.name, field.decName] as [unknown, unknown],
[field.value, field.decValue] as [unknown, unknown],
@@ -1157,6 +1290,9 @@ async function buildCipherPayload(
identity: null,
secureNote: null,
sshKey: null,
bankAccount: null,
driversLicense: null,
passport: null,
fields: await encryptCustomFields(draft.customFields || [], keys.enc, keys.mac),
passwordHistory: await encryptPasswordHistory(cipher?.passwordHistory, keys.enc, keys.mac),
};
@@ -1222,11 +1358,73 @@ async function buildCipherPayload(
} else if (type === 5) {
const encryptedFingerprint = await encryptTextValue(draft.sshFingerprint, keys.enc, keys.mac);
payload.sshKey = {
...stripDecodedObjectFields(cipher?.sshKey),
privateKey: await encryptTextValue(draft.sshPrivateKey, keys.enc, keys.mac),
publicKey: await encryptTextValue(draft.sshPublicKey, keys.enc, keys.mac),
keyFingerprint: encryptedFingerprint,
fingerprint: encryptedFingerprint,
};
} else if (type === 6) {
payload.bankAccount = await encryptObjectFields(
cipher?.bankAccount,
[
['bankName', 'bankName'],
['nameOnAccount', 'bankNameOnAccount'],
['accountType', 'bankAccountType'],
['accountNumber', 'bankAccountNumber'],
['routingNumber', 'bankRoutingNumber'],
['branchNumber', 'bankBranchNumber'],
['pin', 'bankPin'],
['swiftCode', 'bankSwiftCode'],
['iban', 'bankIban'],
['bankContactPhone', 'bankContactPhone'],
],
draft,
keys.enc,
keys.mac
);
} else if (type === 7) {
payload.driversLicense = await encryptObjectFields(
cipher?.driversLicense,
[
['firstName', 'licenseFirstName'],
['middleName', 'licenseMiddleName'],
['lastName', 'licenseLastName'],
['dateOfBirth', 'licenseDateOfBirth'],
['licenseNumber', 'licenseNumber'],
['issuingCountry', 'licenseIssuingCountry'],
['issuingState', 'licenseIssuingState'],
['issueDate', 'licenseIssueDate'],
['expirationDate', 'licenseExpirationDate'],
['issuingAuthority', 'licenseIssuingAuthority'],
['licenseClass', 'licenseClass'],
],
draft,
keys.enc,
keys.mac
);
} else if (type === 8) {
payload.passport = await encryptObjectFields(
cipher?.passport,
[
['surname', 'passportSurname'],
['givenName', 'passportGivenName'],
['dateOfBirth', 'passportDateOfBirth'],
['sex', 'passportSex'],
['birthPlace', 'passportBirthPlace'],
['nationality', 'passportNationality'],
['issuingCountry', 'passportIssuingCountry'],
['passportNumber', 'passportNumber'],
['passportType', 'passportType'],
['nationalIdentificationNumber', 'passportNationalIdentificationNumber'],
['issuingAuthority', 'passportIssuingAuthority'],
['issueDate', 'passportIssueDate'],
['expirationDate', 'passportExpirationDate'],
],
draft,
keys.enc,
keys.mac
);
} else if (type === 2) {
payload.secureNote = { type: 0 };
}
+34 -4
View File
@@ -215,13 +215,13 @@ function mapCipherEncrypted(cipher: Cipher): Record<string, unknown> {
const login = cipher.login;
out.login = login
? {
...cloneValue(login),
...(cloneWithoutDecodedFields(login) || {}),
username: login.username ?? null,
password: login.password ?? null,
totp: login.totp ?? null,
uris: Array.isArray(login.uris)
? login.uris.map((uri) => ({
...cloneValue(uri),
...(cloneWithoutDecodedFields(uri) || {}),
uri: uri?.uri ?? null,
uriChecksum: uri?.uriChecksum ?? null,
match: (uri as { match?: unknown })?.match ?? null,
@@ -280,6 +280,7 @@ function mapCipherEncrypted(cipher: Cipher): Record<string, unknown> {
out.sshKey = cipher.sshKey
? {
...(cloneWithoutDecodedFields(cipher.sshKey) || {}),
privateKey: cipher.sshKey.privateKey ?? null,
publicKey: cipher.sshKey.publicKey ?? null,
keyFingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null,
@@ -287,6 +288,9 @@ function mapCipherEncrypted(cipher: Cipher): Record<string, unknown> {
fingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null,
}
: null;
out.bankAccount = cloneWithoutDecodedFields(cipher.bankAccount) ?? null;
out.driversLicense = cloneWithoutDecodedFields(cipher.driversLicense) ?? null;
out.passport = cloneWithoutDecodedFields(cipher.passport) ?? null;
return out;
}
@@ -331,8 +335,8 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint
out.login = null;
}
out.card = cipher.card ? await deepDecryptUnknown(cipher.card, keyParts.enc, keyParts.mac) : null;
out.identity = cipher.identity ? await deepDecryptUnknown(cipher.identity, keyParts.enc, keyParts.mac) : null;
out.card = cipher.card ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.card), keyParts.enc, keyParts.mac) : null;
out.identity = cipher.identity ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.identity), keyParts.enc, keyParts.mac) : null;
if (cipher.sshKey) {
const fingerprint = await decryptMaybe(
cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null,
@@ -340,6 +344,7 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint
keyParts.mac
);
out.sshKey = {
...((await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.sshKey), keyParts.enc, keyParts.mac)) as Record<string, unknown>),
privateKey: await decryptMaybe(cipher.sshKey.privateKey ?? null, keyParts.enc, keyParts.mac),
publicKey: await decryptMaybe(cipher.sshKey.publicKey ?? null, keyParts.enc, keyParts.mac),
keyFingerprint: fingerprint,
@@ -349,6 +354,15 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint
} else {
out.sshKey = null;
}
out.bankAccount = cipher.bankAccount
? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.bankAccount), keyParts.enc, keyParts.mac)
: null;
out.driversLicense = cipher.driversLicense
? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.driversLicense), keyParts.enc, keyParts.mac)
: null;
out.passport = cipher.passport
? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.passport), keyParts.enc, keyParts.mac)
: null;
out.secureNote = cipher.secureNote
? {
type: normalizeNumber((cipher.secureNote as { type?: unknown }).type, 0),
@@ -431,6 +445,9 @@ function sourceTypeLabel(type: number): string {
if (type === 3) return 'card';
if (type === 4) return 'identity';
if (type === 5) return 'sshKey';
if (type === 6) return 'bankAccount';
if (type === 7) return 'driversLicense';
if (type === 8) return 'passport';
if (type === 2) return 'note';
return `type ${type}`;
}
@@ -449,6 +466,16 @@ function appendRecordFieldLines(lines: string[], prefix: string, value: unknown)
}
}
function cloneWithoutDecodedFields(value: unknown): Record<string, unknown> | null {
if (!isRecord(value)) return null;
const out: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) {
if (/^dec[A-Z]/.test(key)) continue;
out[key] = cloneValue(item);
}
return out;
}
const BITWARDEN_CSV_OBJECT_FIELDS: Record<string, readonly string[]> = {
card: ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code'],
identity: [
@@ -472,6 +499,9 @@ const BITWARDEN_CSV_OBJECT_FIELDS: Record<string, readonly string[]> = {
'country',
],
sshKey: ['privateKey', 'publicKey', 'keyFingerprint', 'fingerprint'],
bankAccount: ['bankName', 'nameOnAccount', 'accountType', 'accountNumber', 'routingNumber', 'branchNumber', 'pin', 'swiftCode', 'iban', 'bankContactPhone'],
driversLicense: ['firstName', 'middleName', 'lastName', 'dateOfBirth', 'licenseNumber', 'issuingCountry', 'issuingState', 'issueDate', 'expirationDate', 'issuingAuthority', 'licenseClass'],
passport: ['surname', 'givenName', 'dateOfBirth', 'sex', 'birthPlace', 'nationality', 'issuingCountry', 'passportNumber', 'passportType', 'nationalIdentificationNumber', 'issuingAuthority', 'issueDate', 'expirationDate'],
};
function appendKnownRecordFieldLines(lines: string[], prefix: string, value: unknown): void {
+29
View File
@@ -688,6 +688,35 @@ const en: Record<string, string> = {
"txt_last_name": "Last Name",
"txt_last_seen": "Last Seen",
"txt_license_number": "License Number",
"txt_bank_account": "Bank Account",
"txt_bank_account_details": "Bank Account Details",
"txt_bank_name": "Bank Name",
"txt_name_on_account": "Name on Account",
"txt_account_type": "Account Type",
"txt_account_number": "Account Number",
"txt_routing_number": "Routing Number",
"txt_branch_number": "Branch Number",
"txt_pin": "PIN",
"txt_swift_code": "SWIFT Code",
"txt_iban": "IBAN",
"txt_bank_contact_phone": "Bank Contact Phone",
"txt_drivers_license": "Driver License",
"txt_drivers_license_details": "Driver License Details",
"txt_date_of_birth": "Date of Birth",
"txt_issuing_country": "Issuing Country",
"txt_issuing_state": "Issuing State",
"txt_issue_date": "Issue Date",
"txt_issuing_authority": "Issuing Authority",
"txt_license_class": "License Class",
"txt_passport": "Passport",
"txt_passport_details": "Passport Details",
"txt_surname": "Surname",
"txt_given_name": "Given Name",
"txt_sex": "Sex",
"txt_birth_place": "Place of Birth",
"txt_nationality": "Nationality",
"txt_passport_type": "Passport Type",
"txt_national_id_number": "National ID Number",
"txt_link_copied": "Link copied",
"txt_linked": "Linked",
"txt_linux_desktop": "Linux Desktop",
+29
View File
@@ -688,6 +688,35 @@ const es: Record<string, string> = {
"txt_last_name": "Apellido",
"txt_last_seen": "Visto por última vez",
"txt_license_number": "Número de licencia",
"txt_bank_account": "Cuenta bancaria",
"txt_bank_account_details": "Detalles de cuenta bancaria",
"txt_bank_name": "Nombre del banco",
"txt_name_on_account": "Nombre en la cuenta",
"txt_account_type": "Tipo de cuenta",
"txt_account_number": "Número de cuenta",
"txt_routing_number": "Número de ruta",
"txt_branch_number": "Número de sucursal",
"txt_pin": "PIN",
"txt_swift_code": "Código SWIFT",
"txt_iban": "IBAN",
"txt_bank_contact_phone": "Teléfono del banco",
"txt_drivers_license": "Licencia de conducir",
"txt_drivers_license_details": "Detalles de licencia de conducir",
"txt_date_of_birth": "Fecha de nacimiento",
"txt_issuing_country": "País emisor",
"txt_issuing_state": "Estado emisor",
"txt_issue_date": "Fecha de emisión",
"txt_issuing_authority": "Autoridad emisora",
"txt_license_class": "Clase de licencia",
"txt_passport": "Pasaporte",
"txt_passport_details": "Detalles del pasaporte",
"txt_surname": "Apellido",
"txt_given_name": "Nombre",
"txt_sex": "Sexo",
"txt_birth_place": "Lugar de nacimiento",
"txt_nationality": "Nacionalidad",
"txt_passport_type": "Tipo de pasaporte",
"txt_national_id_number": "Número de ID nacional",
"txt_link_copied": "Enlace copiado",
"txt_linked": "Vinculado",
"txt_linux_desktop": "Escritorio Linux",
+29
View File
@@ -688,6 +688,35 @@ const ru: Record<string, string> = {
"txt_last_name": "Фамилия",
"txt_last_seen": "Последний визит",
"txt_license_number": "Номер лицензии",
"txt_bank_account": "Банковский счет",
"txt_bank_account_details": "Данные банковского счета",
"txt_bank_name": "Название банка",
"txt_name_on_account": "Имя владельца счета",
"txt_account_type": "Тип счета",
"txt_account_number": "Номер счета",
"txt_routing_number": "Маршрутный номер",
"txt_branch_number": "Номер отделения",
"txt_pin": "PIN",
"txt_swift_code": "SWIFT-код",
"txt_iban": "IBAN",
"txt_bank_contact_phone": "Телефон банка",
"txt_drivers_license": "Водительское удостоверение",
"txt_drivers_license_details": "Данные водительского удостоверения",
"txt_date_of_birth": "Дата рождения",
"txt_issuing_country": "Страна выдачи",
"txt_issuing_state": "Регион выдачи",
"txt_issue_date": "Дата выдачи",
"txt_issuing_authority": "Орган выдачи",
"txt_license_class": "Категория",
"txt_passport": "Паспорт",
"txt_passport_details": "Данные паспорта",
"txt_surname": "Фамилия",
"txt_given_name": "Имя",
"txt_sex": "Пол",
"txt_birth_place": "Место рождения",
"txt_nationality": "Гражданство",
"txt_passport_type": "Тип паспорта",
"txt_national_id_number": "Национальный ID",
"txt_link_copied": "Ссылка скопирована",
"txt_linked": "Связано",
"txt_linux_desktop": "Рабочий стол Linux",
+29
View File
@@ -688,6 +688,35 @@ const zhCN: Record<string, string> = {
"txt_last_name": "姓",
"txt_last_seen": "最后在线",
"txt_license_number": "证件号",
"txt_bank_account": "银行账户",
"txt_bank_account_details": "银行账户详情",
"txt_bank_name": "银行名称",
"txt_name_on_account": "账户姓名",
"txt_account_type": "账户类型",
"txt_account_number": "账户号码",
"txt_routing_number": "路由号码",
"txt_branch_number": "分行号码",
"txt_pin": "PIN",
"txt_swift_code": "SWIFT 代码",
"txt_iban": "IBAN",
"txt_bank_contact_phone": "银行联系电话",
"txt_drivers_license": "驾照",
"txt_drivers_license_details": "驾照详情",
"txt_date_of_birth": "出生日期",
"txt_issuing_country": "签发国家/地区",
"txt_issuing_state": "签发州/省",
"txt_issue_date": "签发日期",
"txt_issuing_authority": "签发机构",
"txt_license_class": "驾照等级",
"txt_passport": "护照",
"txt_passport_details": "护照详情",
"txt_surname": "姓",
"txt_given_name": "名",
"txt_sex": "性别",
"txt_birth_place": "出生地",
"txt_nationality": "国籍",
"txt_passport_type": "护照类型",
"txt_national_id_number": "国家身份证号",
"txt_link_copied": "链接已复制",
"txt_linked": "已关联",
"txt_linux_desktop": "Linux 桌面端",
+29
View File
@@ -688,6 +688,35 @@ const zhTW: Record<string, string> = {
"txt_last_name": "姓",
"txt_last_seen": "最後在線",
"txt_license_number": "證件號",
"txt_bank_account": "銀行帳戶",
"txt_bank_account_details": "銀行帳戶詳情",
"txt_bank_name": "銀行名稱",
"txt_name_on_account": "帳戶姓名",
"txt_account_type": "帳戶類型",
"txt_account_number": "帳戶號碼",
"txt_routing_number": "路由號碼",
"txt_branch_number": "分行號碼",
"txt_pin": "PIN",
"txt_swift_code": "SWIFT 代碼",
"txt_iban": "IBAN",
"txt_bank_contact_phone": "銀行聯絡電話",
"txt_drivers_license": "駕照",
"txt_drivers_license_details": "駕照詳情",
"txt_date_of_birth": "出生日期",
"txt_issuing_country": "簽發國家/地區",
"txt_issuing_state": "簽發州/省",
"txt_issue_date": "簽發日期",
"txt_issuing_authority": "簽發機構",
"txt_license_class": "駕照等級",
"txt_passport": "護照",
"txt_passport_details": "護照詳情",
"txt_surname": "姓",
"txt_given_name": "名",
"txt_sex": "性別",
"txt_birth_place": "出生地",
"txt_nationality": "國籍",
"txt_passport_type": "護照類型",
"txt_national_id_number": "國家身分證號",
"txt_link_copied": "鏈接已複製",
"txt_linked": "已關聯",
"txt_linux_desktop": "Linux 桌面端",
+9 -1
View File
@@ -40,6 +40,10 @@ export interface BitwardenCipherInput {
fields?: BitwardenFieldInput[] | null;
passwordHistory?: Array<{ password?: string | null; lastUsedDate?: string | null }> | null;
sshKey?: Record<string, unknown> | null;
bankAccount?: Record<string, unknown> | null;
driversLicense?: Record<string, unknown> | null;
passport?: Record<string, unknown> | null;
[key: string]: unknown;
}
export interface BitwardenJsonInput {
@@ -79,6 +83,7 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload {
let hasAnyExplicitFolderLink = false;
for (const item of itemsRaw) {
ciphers.push({
...(item && typeof item === 'object' ? item as Record<string, unknown> : {}),
id: item?.id ?? null,
type: Number(item?.type || 1) || 1,
name: item?.name ?? 'Untitled',
@@ -93,7 +98,7 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload {
totp: item.login.totp ?? null,
fido2Credentials: Array.isArray(item.login.fido2Credentials) ? item.login.fido2Credentials : null,
uris: Array.isArray(item.login.uris)
? item.login.uris.map((u) => ({ uri: u?.uri ?? null, match: u?.match ?? null }))
? item.login.uris.map((u) => ({ ...u, uri: u?.uri ?? null, uriChecksum: u?.uriChecksum ?? null, match: u?.match ?? null }))
: null,
}
: null,
@@ -114,6 +119,9 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload {
.filter((x) => !!x.password)
: null,
sshKey: item?.sshKey ?? null,
bankAccount: item?.bankAccount ?? null,
driversLicense: item?.driversLicense ?? null,
passport: item?.passport ?? null,
});
const folderId = txt(item?.folderId);
if (!folderId) continue;
+117
View File
@@ -142,6 +142,86 @@ export interface CipherSshKey {
decFingerprint?: string;
}
export interface CipherBankAccount {
bankName?: string | null;
nameOnAccount?: string | null;
accountType?: string | null;
accountNumber?: string | null;
routingNumber?: string | null;
branchNumber?: string | null;
pin?: string | null;
swiftCode?: string | null;
iban?: string | null;
bankContactPhone?: string | null;
decBankName?: string;
decNameOnAccount?: string;
decAccountType?: string;
decAccountNumber?: string;
decRoutingNumber?: string;
decBranchNumber?: string;
decPin?: string;
decSwiftCode?: string;
decIban?: string;
decBankContactPhone?: string;
[key: string]: unknown;
}
export interface CipherDriversLicense {
firstName?: string | null;
middleName?: string | null;
lastName?: string | null;
dateOfBirth?: string | null;
licenseNumber?: string | null;
issuingCountry?: string | null;
issuingState?: string | null;
issueDate?: string | null;
expirationDate?: string | null;
issuingAuthority?: string | null;
licenseClass?: string | null;
decFirstName?: string;
decMiddleName?: string;
decLastName?: string;
decDateOfBirth?: string;
decLicenseNumber?: string;
decIssuingCountry?: string;
decIssuingState?: string;
decIssueDate?: string;
decExpirationDate?: string;
decIssuingAuthority?: string;
decLicenseClass?: string;
[key: string]: unknown;
}
export interface CipherPassport {
surname?: string | null;
givenName?: string | null;
dateOfBirth?: string | null;
sex?: string | null;
birthPlace?: string | null;
nationality?: string | null;
issuingCountry?: string | null;
passportNumber?: string | null;
passportType?: string | null;
nationalIdentificationNumber?: string | null;
issuingAuthority?: string | null;
issueDate?: string | null;
expirationDate?: string | null;
decSurname?: string;
decGivenName?: string;
decDateOfBirth?: string;
decSex?: string;
decBirthPlace?: string;
decNationality?: string;
decIssuingCountry?: string;
decPassportNumber?: string;
decPassportType?: string;
decNationalIdentificationNumber?: string;
decIssuingAuthority?: string;
decIssueDate?: string;
decExpirationDate?: string;
[key: string]: unknown;
}
export interface CipherField {
type?: number | string | null;
name?: string | null;
@@ -175,6 +255,9 @@ export interface Cipher {
card?: CipherCard | null;
identity?: CipherIdentity | null;
sshKey?: CipherSshKey | null;
bankAccount?: CipherBankAccount | null;
driversLicense?: CipherDriversLicense | null;
passport?: CipherPassport | null;
secureNote?: { type?: number | null } | null;
passwordHistory?: CipherPasswordHistoryEntry[] | null;
fields?: CipherField[] | null;
@@ -276,6 +359,40 @@ export interface VaultDraft {
sshPrivateKey: string;
sshPublicKey: string;
sshFingerprint: string;
bankName: string;
bankNameOnAccount: string;
bankAccountType: string;
bankAccountNumber: string;
bankRoutingNumber: string;
bankBranchNumber: string;
bankPin: string;
bankSwiftCode: string;
bankIban: string;
bankContactPhone: string;
licenseFirstName: string;
licenseMiddleName: string;
licenseLastName: string;
licenseDateOfBirth: string;
licenseNumber: string;
licenseIssuingCountry: string;
licenseIssuingState: string;
licenseIssueDate: string;
licenseExpirationDate: string;
licenseIssuingAuthority: string;
licenseClass: string;
passportSurname: string;
passportGivenName: string;
passportDateOfBirth: string;
passportSex: string;
passportBirthPlace: string;
passportNationality: string;
passportIssuingCountry: string;
passportNumber: string;
passportType: string;
passportNationalIdentificationNumber: string;
passportIssuingAuthority: string;
passportIssueDate: string;
passportExpirationDate: string;
customFields: VaultDraftField[];
}
+61
View File
@@ -66,6 +66,31 @@ async function decryptCipherField(
return looksLikeCipherString(value) ? '' : value;
}
async function decryptCipherObjectFields<T extends Record<string, unknown>>(
source: T | null | undefined,
fields: readonly string[],
itemEnc: Uint8Array,
itemMac: Uint8Array,
userEnc: Uint8Array,
userMac: Uint8Array,
canFallbackToUserKey: boolean
): Promise<T | null | undefined> {
if (!source || typeof source !== 'object') return source;
const next: Record<string, unknown> = { ...source };
for (const field of fields) {
const decKey = `dec${field.charAt(0).toUpperCase()}${field.slice(1)}`;
next[decKey] = await decryptCipherField(
source[field] as string | null | undefined,
itemEnc,
itemMac,
userEnc,
userMac,
canFallbackToUserKey
);
}
return next as T;
}
async function decryptFieldWithSource(
value: string | null | undefined,
itemEnc: Uint8Array,
@@ -200,6 +225,42 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
};
}
if (cipher.bankAccount) {
nextCipher.bankAccount = await decryptCipherObjectFields(
cipher.bankAccount,
['bankName', 'nameOnAccount', 'accountType', 'accountNumber', 'routingNumber', 'branchNumber', 'pin', 'swiftCode', 'iban', 'bankContactPhone'],
itemEnc,
itemMac,
userEnc,
userMac,
canFallbackToUserKey
);
}
if (cipher.driversLicense) {
nextCipher.driversLicense = await decryptCipherObjectFields(
cipher.driversLicense,
['firstName', 'middleName', 'lastName', 'dateOfBirth', 'licenseNumber', 'issuingCountry', 'issuingState', 'issueDate', 'expirationDate', 'issuingAuthority', 'licenseClass'],
itemEnc,
itemMac,
userEnc,
userMac,
canFallbackToUserKey
);
}
if (cipher.passport) {
nextCipher.passport = await decryptCipherObjectFields(
cipher.passport,
['surname', 'givenName', 'dateOfBirth', 'sex', 'birthPlace', 'nationality', 'issuingCountry', 'passportNumber', 'passportType', 'nationalIdentificationNumber', 'issuingAuthority', 'issueDate', 'expirationDate'],
itemEnc,
itemMac,
userEnc,
userMac,
canFallbackToUserKey
);
}
if (cipher.fields) {
nextCipher.fields = await Promise.all(
cipher.fields.map(async (field) => ({