mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
fix: repair mixed cipher key encryption handling
This commit is contained in:
@@ -47,3 +47,4 @@ AGENTS.md
|
||||
settings.json
|
||||
.claude/
|
||||
NodeWarden-compat/
|
||||
.codex-upstream/
|
||||
|
||||
@@ -148,6 +148,10 @@
|
||||
compatibility: {
|
||||
// Single source of truth for /config.version and /api/version.
|
||||
// /config.version 与 /api/version 的统一版本号来源。
|
||||
bitwardenServerVersion: '2026.1.0',
|
||||
bitwardenServerVersion: '2026.4.1',
|
||||
// Advertise official per-cipher item-key encryption support only after
|
||||
// NodeWarden can guarantee key/field consistency across all write paths.
|
||||
// 在所有写入路径都能保证 cipher.key 与字段密文一致之前,不向官方客户端声明支持逐项密钥加密。
|
||||
cipherKeyEncryptionFeatureEnabled: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -115,7 +115,7 @@ function buildConfigResponse(origin: string) {
|
||||
_icon_service_url: buildIconServiceTemplate(origin),
|
||||
_icon_service_csp: buildIconServiceCsp(origin),
|
||||
featureStates: {
|
||||
'cipher-key-encryption': true,
|
||||
'cipher-key-encryption': LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled,
|
||||
'duo-redirect': true,
|
||||
'email-verification': true,
|
||||
'pm-19051-send-email-verification': false,
|
||||
|
||||
+7
-4
@@ -25,7 +25,7 @@ import {
|
||||
import { clearAuditLogs, getAuditLogSettings, listAdminInvites, listAdminUsers, listAuditLogs, saveAuditLogSettings, type AuditLogFilters } from '@/lib/api/admin';
|
||||
import { getDomainRules, saveDomainRules } from '@/lib/api/domains';
|
||||
import { getSends } from '@/lib/api/send';
|
||||
import { repairCipherUriChecksums } from '@/lib/api/vault';
|
||||
import { repairCipherKeyMismatches, repairCipherUriChecksums } from '@/lib/api/vault';
|
||||
import { getCachedVaultCoreSnapshot, loadVaultCoreSyncSnapshot } from '@/lib/api/vault-sync';
|
||||
import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair';
|
||||
import {
|
||||
@@ -1086,9 +1086,12 @@ export default function App() {
|
||||
const repairKey = `${session.accessToken}:${encryptedCiphers.map((cipher) => `${cipher.id}:${cipher.revisionDate || ''}`).join(',')}`;
|
||||
if (uriChecksumRepairAttemptRef.current !== repairKey) {
|
||||
uriChecksumRepairAttemptRef.current = repairKey;
|
||||
void repairCipherUriChecksums(authedFetch, session, result.ciphers)
|
||||
.then((count) => {
|
||||
if (count > 0) void refetchVaultCoreData();
|
||||
void Promise.all([
|
||||
repairCipherKeyMismatches(authedFetch, session, result.ciphers),
|
||||
repairCipherUriChecksums(authedFetch, session, result.ciphers),
|
||||
])
|
||||
.then(([keyMismatchCount, uriChecksumCount]) => {
|
||||
if (keyMismatchCount + uriChecksumCount > 0) void refetchVaultCoreData();
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort compatibility repair must not interrupt normal vault loading.
|
||||
|
||||
+287
-1
@@ -496,8 +496,11 @@ async function encryptPasswordHistory(
|
||||
const out: CipherPasswordHistoryEntry[] = [];
|
||||
for (const entry of entries) {
|
||||
const rawPassword = String(entry?.password || '');
|
||||
const hasDecryptedPassword = typeof entry?.decPassword === 'string';
|
||||
const plainPassword = entry?.decPassword ?? rawPassword;
|
||||
const encryptedPassword = looksLikeCipherString(rawPassword)
|
||||
const encryptedPassword = hasDecryptedPassword
|
||||
? await encryptTextValue(plainPassword, enc, mac)
|
||||
: looksLikeCipherString(rawPassword)
|
||||
? rawPassword
|
||||
: await encryptTextValue(plainPassword, enc, mac);
|
||||
if (!encryptedPassword) continue;
|
||||
@@ -510,6 +513,133 @@ async function encryptPasswordHistory(
|
||||
return out.length ? out : null;
|
||||
}
|
||||
|
||||
function plainCipherValue(decrypted: unknown, raw: unknown = ''): string {
|
||||
if (typeof decrypted === 'string' && !looksLikeCipherString(decrypted)) return decrypted;
|
||||
const value = String(raw ?? '');
|
||||
return looksLikeCipherString(value) ? '' : value;
|
||||
}
|
||||
|
||||
function draftFromDecryptedCipher(cipher: Cipher): VaultDraft {
|
||||
const type = Number(cipher.type || 1) || 1;
|
||||
const draft: VaultDraft = {
|
||||
type,
|
||||
name: plainCipherValue(cipher.decName, cipher.name).trim() || 'Untitled',
|
||||
notes: plainCipherValue(cipher.decNotes, cipher.notes),
|
||||
favorite: !!cipher.favorite,
|
||||
reprompt: Number(cipher.reprompt || 0) === 1,
|
||||
folderId: cipher.folderId || '',
|
||||
loginUsername: '',
|
||||
loginPassword: '',
|
||||
loginTotp: '',
|
||||
loginUris: [{ uri: '', match: null, originalUri: '', extra: {} }],
|
||||
loginFido2Credentials: [],
|
||||
cardholderName: '',
|
||||
cardNumber: '',
|
||||
cardBrand: '',
|
||||
cardExpMonth: '',
|
||||
cardExpYear: '',
|
||||
cardCode: '',
|
||||
identTitle: '',
|
||||
identFirstName: '',
|
||||
identMiddleName: '',
|
||||
identLastName: '',
|
||||
identUsername: '',
|
||||
identCompany: '',
|
||||
identSsn: '',
|
||||
identPassportNumber: '',
|
||||
identLicenseNumber: '',
|
||||
identEmail: '',
|
||||
identPhone: '',
|
||||
identAddress1: '',
|
||||
identAddress2: '',
|
||||
identAddress3: '',
|
||||
identCity: '',
|
||||
identState: '',
|
||||
identPostalCode: '',
|
||||
identCountry: '',
|
||||
sshPrivateKey: '',
|
||||
sshPublicKey: '',
|
||||
sshFingerprint: '',
|
||||
customFields: [],
|
||||
};
|
||||
|
||||
draft.customFields = (cipher.fields || [])
|
||||
.map((field) => ({
|
||||
type: parseFieldType(field.type ?? 0),
|
||||
label: plainCipherValue(field.decName, field.name).trim(),
|
||||
value: plainCipherValue(field.decValue, field.value),
|
||||
}))
|
||||
.filter((field) => field.label);
|
||||
|
||||
if (type === 1 && cipher.login) {
|
||||
draft.loginUsername = plainCipherValue(cipher.login.decUsername, cipher.login.username);
|
||||
draft.loginPassword = plainCipherValue(cipher.login.decPassword, cipher.login.password);
|
||||
draft.loginTotp = plainCipherValue(cipher.login.decTotp, cipher.login.totp);
|
||||
draft.loginFido2Credentials = Array.isArray(cipher.login.fido2Credentials)
|
||||
? cipher.login.fido2Credentials.filter((item): item is Record<string, unknown> => !!item && typeof item === 'object')
|
||||
: [];
|
||||
const seenUris = new Set<string>();
|
||||
const uris = (cipher.login.uris || [])
|
||||
.map((entry) => {
|
||||
const uri = plainCipherValue(entry.decUri, entry.uri).trim();
|
||||
const extra = { ...(entry as Record<string, unknown>) };
|
||||
delete extra.uri;
|
||||
delete extra.uriChecksum;
|
||||
delete extra.match;
|
||||
delete extra.decUri;
|
||||
return {
|
||||
uri,
|
||||
match: typeof entry.match === 'number' && Number.isFinite(entry.match) ? entry.match : null,
|
||||
originalUri: '',
|
||||
extra,
|
||||
};
|
||||
})
|
||||
.filter((entry) => {
|
||||
if (!entry.uri) return false;
|
||||
const key = entry.uri.toLowerCase();
|
||||
if (seenUris.has(key)) return false;
|
||||
seenUris.add(key);
|
||||
return true;
|
||||
});
|
||||
draft.loginUris = uris.length ? uris : draft.loginUris;
|
||||
} else if (type === 3 && cipher.card) {
|
||||
draft.cardholderName = plainCipherValue(cipher.card.decCardholderName, cipher.card.cardholderName);
|
||||
draft.cardNumber = plainCipherValue(cipher.card.decNumber, cipher.card.number);
|
||||
draft.cardBrand = plainCipherValue(cipher.card.decBrand, cipher.card.brand);
|
||||
draft.cardExpMonth = plainCipherValue(cipher.card.decExpMonth, cipher.card.expMonth);
|
||||
draft.cardExpYear = plainCipherValue(cipher.card.decExpYear, cipher.card.expYear);
|
||||
draft.cardCode = plainCipherValue(cipher.card.decCode, cipher.card.code);
|
||||
} else if (type === 4 && cipher.identity) {
|
||||
draft.identTitle = plainCipherValue(cipher.identity.decTitle, cipher.identity.title);
|
||||
draft.identFirstName = plainCipherValue(cipher.identity.decFirstName, cipher.identity.firstName);
|
||||
draft.identMiddleName = plainCipherValue(cipher.identity.decMiddleName, cipher.identity.middleName);
|
||||
draft.identLastName = plainCipherValue(cipher.identity.decLastName, cipher.identity.lastName);
|
||||
draft.identUsername = plainCipherValue(cipher.identity.decUsername, cipher.identity.username);
|
||||
draft.identCompany = plainCipherValue(cipher.identity.decCompany, cipher.identity.company);
|
||||
draft.identSsn = plainCipherValue(cipher.identity.decSsn, cipher.identity.ssn);
|
||||
draft.identPassportNumber = plainCipherValue(cipher.identity.decPassportNumber, cipher.identity.passportNumber);
|
||||
draft.identLicenseNumber = plainCipherValue(cipher.identity.decLicenseNumber, cipher.identity.licenseNumber);
|
||||
draft.identEmail = plainCipherValue(cipher.identity.decEmail, cipher.identity.email);
|
||||
draft.identPhone = plainCipherValue(cipher.identity.decPhone, cipher.identity.phone);
|
||||
draft.identAddress1 = plainCipherValue(cipher.identity.decAddress1, cipher.identity.address1);
|
||||
draft.identAddress2 = plainCipherValue(cipher.identity.decAddress2, cipher.identity.address2);
|
||||
draft.identAddress3 = plainCipherValue(cipher.identity.decAddress3, cipher.identity.address3);
|
||||
draft.identCity = plainCipherValue(cipher.identity.decCity, cipher.identity.city);
|
||||
draft.identState = plainCipherValue(cipher.identity.decState, cipher.identity.state);
|
||||
draft.identPostalCode = plainCipherValue(cipher.identity.decPostalCode, cipher.identity.postalCode);
|
||||
draft.identCountry = plainCipherValue(cipher.identity.decCountry, cipher.identity.country);
|
||||
} else if (type === 5 && cipher.sshKey) {
|
||||
draft.sshPrivateKey = plainCipherValue(cipher.sshKey.decPrivateKey, cipher.sshKey.privateKey);
|
||||
draft.sshPublicKey = plainCipherValue(cipher.sshKey.decPublicKey, cipher.sshKey.publicKey);
|
||||
draft.sshFingerprint = plainCipherValue(
|
||||
cipher.sshKey.decFingerprint,
|
||||
cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
async function buildUpdatedPasswordHistory(
|
||||
cipher: Cipher | null,
|
||||
draft: VaultDraft,
|
||||
@@ -798,6 +928,162 @@ export async function repairCipherUriChecksums(
|
||||
return repaired;
|
||||
}
|
||||
|
||||
function getCipherKeyMismatchProbes(cipher: Cipher): string[] {
|
||||
const candidates = [
|
||||
cipher.name,
|
||||
cipher.notes,
|
||||
cipher.login?.username,
|
||||
cipher.login?.password,
|
||||
cipher.login?.totp,
|
||||
...(cipher.login?.uris || []).map((uri) => uri.uri),
|
||||
cipher.card?.cardholderName,
|
||||
cipher.card?.number,
|
||||
cipher.identity?.title,
|
||||
cipher.identity?.firstName,
|
||||
cipher.sshKey?.privateKey,
|
||||
...(cipher.fields || []).flatMap((field) => [field.name, field.value]),
|
||||
];
|
||||
const probes: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const value of candidates) {
|
||||
const probe = String(value || '').trim();
|
||||
if (!looksLikeCipherString(probe) || seen.has(probe)) continue;
|
||||
seen.add(probe);
|
||||
probes.push(probe);
|
||||
}
|
||||
return probes;
|
||||
}
|
||||
|
||||
function isResolvedEncryptedField(raw: unknown, decrypted: unknown): boolean {
|
||||
const encrypted = String(raw || '').trim();
|
||||
if (!looksLikeCipherString(encrypted)) return true;
|
||||
const plain = typeof decrypted === 'string' ? decrypted.trim() : '';
|
||||
return !!plain && !looksLikeCipherString(plain);
|
||||
}
|
||||
|
||||
function hasUnresolvedEncryptedFields(cipher: Cipher): boolean {
|
||||
const fido2EncryptedFields = (cipher.login?.fido2Credentials || []).flatMap((credential) => [
|
||||
credential?.credentialId,
|
||||
credential?.keyType,
|
||||
credential?.keyAlgorithm,
|
||||
credential?.keyCurve,
|
||||
credential?.keyValue,
|
||||
credential?.rpId,
|
||||
credential?.rpName,
|
||||
credential?.userHandle,
|
||||
credential?.userName,
|
||||
credential?.userDisplayName,
|
||||
credential?.counter,
|
||||
credential?.discoverable,
|
||||
]);
|
||||
|
||||
const checks: Array<[unknown, unknown]> = [
|
||||
[cipher.name, cipher.decName],
|
||||
[cipher.notes, cipher.decNotes],
|
||||
[cipher.login?.username, cipher.login?.decUsername],
|
||||
[cipher.login?.password, cipher.login?.decPassword],
|
||||
[cipher.login?.totp, cipher.login?.decTotp],
|
||||
...(cipher.login?.uris || []).map((uri) => [uri.uri, uri.decUri] as [unknown, unknown]),
|
||||
[cipher.card?.cardholderName, cipher.card?.decCardholderName],
|
||||
[cipher.card?.number, cipher.card?.decNumber],
|
||||
[cipher.card?.brand, cipher.card?.decBrand],
|
||||
[cipher.card?.expMonth, cipher.card?.decExpMonth],
|
||||
[cipher.card?.expYear, cipher.card?.decExpYear],
|
||||
[cipher.card?.code, cipher.card?.decCode],
|
||||
[cipher.identity?.title, cipher.identity?.decTitle],
|
||||
[cipher.identity?.firstName, cipher.identity?.decFirstName],
|
||||
[cipher.identity?.middleName, cipher.identity?.decMiddleName],
|
||||
[cipher.identity?.lastName, cipher.identity?.decLastName],
|
||||
[cipher.identity?.username, cipher.identity?.decUsername],
|
||||
[cipher.identity?.company, cipher.identity?.decCompany],
|
||||
[cipher.identity?.ssn, cipher.identity?.decSsn],
|
||||
[cipher.identity?.passportNumber, cipher.identity?.decPassportNumber],
|
||||
[cipher.identity?.licenseNumber, cipher.identity?.decLicenseNumber],
|
||||
[cipher.identity?.email, cipher.identity?.decEmail],
|
||||
[cipher.identity?.phone, cipher.identity?.decPhone],
|
||||
[cipher.identity?.address1, cipher.identity?.decAddress1],
|
||||
[cipher.identity?.address2, cipher.identity?.decAddress2],
|
||||
[cipher.identity?.address3, cipher.identity?.decAddress3],
|
||||
[cipher.identity?.city, cipher.identity?.decCity],
|
||||
[cipher.identity?.state, cipher.identity?.decState],
|
||||
[cipher.identity?.postalCode, cipher.identity?.decPostalCode],
|
||||
[cipher.identity?.country, cipher.identity?.decCountry],
|
||||
[cipher.sshKey?.privateKey, cipher.sshKey?.decPrivateKey],
|
||||
[cipher.sshKey?.publicKey, cipher.sshKey?.decPublicKey],
|
||||
[cipher.sshKey?.keyFingerprint || cipher.sshKey?.fingerprint, cipher.sshKey?.decFingerprint],
|
||||
...(cipher.fields || []).flatMap((field) => [
|
||||
[field.name, field.decName] as [unknown, unknown],
|
||||
[field.value, field.decValue] as [unknown, unknown],
|
||||
]),
|
||||
...(cipher.passwordHistory || []).map((entry) => [entry.password, entry.decPassword] as [unknown, unknown]),
|
||||
...fido2EncryptedFields.map((value) => [value, undefined] as [unknown, unknown]),
|
||||
];
|
||||
|
||||
return checks.some(([raw, decrypted]) => !isResolvedEncryptedField(raw, decrypted));
|
||||
}
|
||||
|
||||
async function hasItemKeyFieldMismatch(
|
||||
cipher: Cipher,
|
||||
userEnc: Uint8Array,
|
||||
userMac: Uint8Array
|
||||
): Promise<boolean> {
|
||||
if (!looksLikeCipherString(cipher.key)) return false;
|
||||
const probes = getCipherKeyMismatchProbes(cipher);
|
||||
if (probes.length === 0) return false;
|
||||
|
||||
let itemKey: Uint8Array;
|
||||
try {
|
||||
itemKey = await decryptBw(String(cipher.key).trim(), userEnc, userMac);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (itemKey.length < 64) return false;
|
||||
|
||||
const itemEnc = itemKey.slice(0, 32);
|
||||
const itemMac = itemKey.slice(32, 64);
|
||||
for (const probe of probes) {
|
||||
try {
|
||||
await decryptStr(probe, itemEnc, itemMac);
|
||||
continue;
|
||||
} catch {
|
||||
// Try the legacy user-key field path below.
|
||||
}
|
||||
|
||||
try {
|
||||
await decryptStr(probe, userEnc, userMac);
|
||||
return true;
|
||||
} catch {
|
||||
// Keep scanning in case another field reveals a repairable mismatch.
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function repairCipherKeyMismatches(
|
||||
authedFetch: AuthedFetch,
|
||||
session: SessionState,
|
||||
ciphers: Cipher[]
|
||||
): Promise<number> {
|
||||
if (!session.symEncKey || !session.symMacKey || !Array.isArray(ciphers) || ciphers.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const userEnc = base64ToBytes(session.symEncKey);
|
||||
const userMac = base64ToBytes(session.symMacKey);
|
||||
let repaired = 0;
|
||||
|
||||
for (const cipher of ciphers) {
|
||||
if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue;
|
||||
if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue;
|
||||
if (hasUnresolvedEncryptedFields(cipher)) continue;
|
||||
await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher));
|
||||
repaired += 1;
|
||||
}
|
||||
|
||||
return repaired;
|
||||
}
|
||||
|
||||
async function buildCipherPayload(
|
||||
session: SessionState,
|
||||
draft: VaultDraft,
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import { decryptStr, decryptBw } from './crypto';
|
||||
import type { Cipher } from './types';
|
||||
|
||||
async function decryptField(
|
||||
async function decryptCipherField(
|
||||
value: string | null | undefined,
|
||||
enc: Uint8Array,
|
||||
mac: Uint8Array,
|
||||
itemEnc: Uint8Array,
|
||||
itemMac: Uint8Array,
|
||||
userEnc: Uint8Array,
|
||||
userMac: Uint8Array,
|
||||
canFallbackToUserKey: boolean,
|
||||
): Promise<string> {
|
||||
if (!value || typeof value !== 'string') return '';
|
||||
try { return await decryptStr(value, enc, mac); } catch { return value; }
|
||||
try {
|
||||
return await decryptStr(value, itemEnc, itemMac);
|
||||
} catch {
|
||||
// Try the legacy user-key path for mixed key/field ciphers.
|
||||
}
|
||||
if (canFallbackToUserKey) {
|
||||
try {
|
||||
return await decryptStr(value, userEnc, userMac);
|
||||
} catch {
|
||||
// Preserve the old raw fallback for fields that are genuinely unreadable.
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function decryptSingleCipher(
|
||||
@@ -17,29 +32,35 @@ export async function decryptSingleCipher(
|
||||
): Promise<Cipher> {
|
||||
let itemEnc = userEnc;
|
||||
let itemMac = userMac;
|
||||
let usesItemKey = false;
|
||||
if (encrypted.key) {
|
||||
try {
|
||||
const itemKey = await decryptBw(encrypted.key, userEnc, userMac);
|
||||
if (itemKey.length >= 64) {
|
||||
itemEnc = itemKey.slice(0, 32);
|
||||
itemMac = itemKey.slice(32, 64);
|
||||
usesItemKey = true;
|
||||
}
|
||||
} catch { /* keep user key */ }
|
||||
}
|
||||
|
||||
const canFallbackToUserKey = usesItemKey;
|
||||
|
||||
const decrypted: Cipher = {
|
||||
...encrypted,
|
||||
decName: await decryptField(encrypted.name, itemEnc, itemMac),
|
||||
decNotes: await decryptField(encrypted.notes, itemEnc, itemMac),
|
||||
decName: await decryptCipherField(encrypted.name, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decNotes: await decryptCipherField(encrypted.notes, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
|
||||
if (encrypted.login) {
|
||||
decrypted.login = {
|
||||
...encrypted.login,
|
||||
decUsername: await decryptField(encrypted.login.username, itemEnc, itemMac),
|
||||
decPassword: await decryptField(encrypted.login.password, itemEnc, itemMac),
|
||||
decTotp: await decryptField(encrypted.login.totp, itemEnc, itemMac),
|
||||
decUsername: await decryptCipherField(encrypted.login.username, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPassword: await decryptCipherField(encrypted.login.password, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decTotp: await decryptCipherField(encrypted.login.totp, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
uris: await Promise.all((encrypted.login.uris || []).map(async (u) => ({
|
||||
...u,
|
||||
decUri: await decryptField(u.uri, itemEnc, itemMac),
|
||||
decUri: await decryptCipherField(u.uri, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))),
|
||||
};
|
||||
}
|
||||
@@ -48,7 +69,7 @@ export async function decryptSingleCipher(
|
||||
decrypted.passwordHistory = await Promise.all(
|
||||
encrypted.passwordHistory.map(async (entry) => ({
|
||||
...entry,
|
||||
decPassword: await decryptField(entry?.password, itemEnc, itemMac),
|
||||
decPassword: await decryptCipherField(entry?.password, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -56,36 +77,36 @@ export async function decryptSingleCipher(
|
||||
if (encrypted.card) {
|
||||
decrypted.card = {
|
||||
...encrypted.card,
|
||||
decCardholderName: await decryptField(encrypted.card.cardholderName, itemEnc, itemMac),
|
||||
decNumber: await decryptField(encrypted.card.number, itemEnc, itemMac),
|
||||
decBrand: await decryptField(encrypted.card.brand, itemEnc, itemMac),
|
||||
decExpMonth: await decryptField(encrypted.card.expMonth, itemEnc, itemMac),
|
||||
decExpYear: await decryptField(encrypted.card.expYear, itemEnc, itemMac),
|
||||
decCode: await decryptField(encrypted.card.code, itemEnc, itemMac),
|
||||
decCardholderName: await decryptCipherField(encrypted.card.cardholderName, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decNumber: await decryptCipherField(encrypted.card.number, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decBrand: await decryptCipherField(encrypted.card.brand, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decExpMonth: await decryptCipherField(encrypted.card.expMonth, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decExpYear: await decryptCipherField(encrypted.card.expYear, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCode: await decryptCipherField(encrypted.card.code, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
if (encrypted.identity) {
|
||||
decrypted.identity = {
|
||||
...encrypted.identity,
|
||||
decTitle: await decryptField(encrypted.identity.title, itemEnc, itemMac),
|
||||
decFirstName: await decryptField(encrypted.identity.firstName, itemEnc, itemMac),
|
||||
decMiddleName: await decryptField(encrypted.identity.middleName, itemEnc, itemMac),
|
||||
decLastName: await decryptField(encrypted.identity.lastName, itemEnc, itemMac),
|
||||
decUsername: await decryptField(encrypted.identity.username, itemEnc, itemMac),
|
||||
decCompany: await decryptField(encrypted.identity.company, itemEnc, itemMac),
|
||||
decSsn: await decryptField(encrypted.identity.ssn, itemEnc, itemMac),
|
||||
decPassportNumber: await decryptField(encrypted.identity.passportNumber, itemEnc, itemMac),
|
||||
decLicenseNumber: await decryptField(encrypted.identity.licenseNumber, itemEnc, itemMac),
|
||||
decEmail: await decryptField(encrypted.identity.email, itemEnc, itemMac),
|
||||
decPhone: await decryptField(encrypted.identity.phone, itemEnc, itemMac),
|
||||
decAddress1: await decryptField(encrypted.identity.address1, itemEnc, itemMac),
|
||||
decAddress2: await decryptField(encrypted.identity.address2, itemEnc, itemMac),
|
||||
decAddress3: await decryptField(encrypted.identity.address3, itemEnc, itemMac),
|
||||
decCity: await decryptField(encrypted.identity.city, itemEnc, itemMac),
|
||||
decState: await decryptField(encrypted.identity.state, itemEnc, itemMac),
|
||||
decPostalCode: await decryptField(encrypted.identity.postalCode, itemEnc, itemMac),
|
||||
decCountry: await decryptField(encrypted.identity.country, itemEnc, itemMac),
|
||||
decTitle: await decryptCipherField(encrypted.identity.title, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decFirstName: await decryptCipherField(encrypted.identity.firstName, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decMiddleName: await decryptCipherField(encrypted.identity.middleName, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decLastName: await decryptCipherField(encrypted.identity.lastName, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decUsername: await decryptCipherField(encrypted.identity.username, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCompany: await decryptCipherField(encrypted.identity.company, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decSsn: await decryptCipherField(encrypted.identity.ssn, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPassportNumber: await decryptCipherField(encrypted.identity.passportNumber, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decLicenseNumber: await decryptCipherField(encrypted.identity.licenseNumber, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decEmail: await decryptCipherField(encrypted.identity.email, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPhone: await decryptCipherField(encrypted.identity.phone, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress1: await decryptCipherField(encrypted.identity.address1, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress2: await decryptCipherField(encrypted.identity.address2, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress3: await decryptCipherField(encrypted.identity.address3, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCity: await decryptCipherField(encrypted.identity.city, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decState: await decryptCipherField(encrypted.identity.state, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPostalCode: await decryptCipherField(encrypted.identity.postalCode, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCountry: await decryptCipherField(encrypted.identity.country, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,11 +114,11 @@ export async function decryptSingleCipher(
|
||||
const fingerprint = encrypted.sshKey.keyFingerprint || encrypted.sshKey.fingerprint || '';
|
||||
decrypted.sshKey = {
|
||||
...encrypted.sshKey,
|
||||
decPrivateKey: await decryptField(encrypted.sshKey.privateKey, itemEnc, itemMac),
|
||||
decPublicKey: await decryptField(encrypted.sshKey.publicKey, itemEnc, itemMac),
|
||||
decPrivateKey: await decryptCipherField(encrypted.sshKey.privateKey, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPublicKey: await decryptCipherField(encrypted.sshKey.publicKey, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
keyFingerprint: fingerprint || null,
|
||||
fingerprint: fingerprint || null,
|
||||
decFingerprint: await decryptField(fingerprint, itemEnc, itemMac),
|
||||
decFingerprint: await decryptCipherField(fingerprint, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,8 +126,8 @@ export async function decryptSingleCipher(
|
||||
decrypted.fields = await Promise.all(
|
||||
encrypted.fields.map(async (field) => ({
|
||||
...field,
|
||||
decName: await decryptField(field.name, itemEnc, itemMac),
|
||||
decValue: await decryptField(field.value, itemEnc, itemMac),
|
||||
decName: await decryptCipherField(field.name, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decValue: await decryptCipherField(field.value, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,30 @@ async function decryptField(
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptCipherField(
|
||||
value: string | null | undefined,
|
||||
itemEnc: Uint8Array,
|
||||
itemMac: Uint8Array,
|
||||
userEnc: Uint8Array,
|
||||
userMac: Uint8Array,
|
||||
canFallbackToUserKey: boolean
|
||||
): Promise<string> {
|
||||
if (!value || typeof value !== 'string') return '';
|
||||
try {
|
||||
return await decryptStr(value, itemEnc, itemMac);
|
||||
} catch {
|
||||
// Try the legacy user-key path for mixed key/field ciphers.
|
||||
}
|
||||
if (canFallbackToUserKey) {
|
||||
try {
|
||||
return await decryptStr(value, userEnc, userMac);
|
||||
} catch {
|
||||
// Preserve the old raw fallback for fields that are genuinely unreadable.
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function decryptFieldWithSource(
|
||||
value: string | null | undefined,
|
||||
itemEnc: Uint8Array,
|
||||
@@ -82,32 +106,38 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
|
||||
args.ciphers.map(async (cipher) => {
|
||||
let itemEnc = userEnc;
|
||||
let itemMac = userMac;
|
||||
let usesItemKey = false;
|
||||
if (cipher.key) {
|
||||
try {
|
||||
const itemKey = await decryptBw(cipher.key, userEnc, userMac);
|
||||
if (itemKey.length >= 64) {
|
||||
itemEnc = itemKey.slice(0, 32);
|
||||
itemMac = itemKey.slice(32, 64);
|
||||
usesItemKey = true;
|
||||
}
|
||||
} catch {
|
||||
// Keep user key fallback.
|
||||
}
|
||||
}
|
||||
|
||||
const itemUsesUserKey = sameBytes(itemEnc, userEnc) && sameBytes(itemMac, userMac);
|
||||
const canFallbackToUserKey = usesItemKey;
|
||||
const nextCipher: Cipher = {
|
||||
...cipher,
|
||||
decName: await decryptField(cipher.name || '', itemEnc, itemMac),
|
||||
decNotes: await decryptField(cipher.notes || '', itemEnc, itemMac),
|
||||
decName: await decryptCipherField(cipher.name || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decNotes: await decryptCipherField(cipher.notes || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
|
||||
if (cipher.login) {
|
||||
nextCipher.login = {
|
||||
...cipher.login,
|
||||
decUsername: await decryptField(cipher.login.username || '', itemEnc, itemMac),
|
||||
decPassword: await decryptField(cipher.login.password || '', itemEnc, itemMac),
|
||||
decTotp: await decryptField(cipher.login.totp || '', itemEnc, itemMac),
|
||||
decUsername: await decryptCipherField(cipher.login.username || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPassword: await decryptCipherField(cipher.login.password || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decTotp: await decryptCipherField(cipher.login.totp || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
uris: await Promise.all(
|
||||
(cipher.login.uris || []).map(async (uri) => ({
|
||||
...uri,
|
||||
decUri: await decryptField(uri.uri || '', itemEnc, itemMac),
|
||||
decUri: await decryptCipherField(uri.uri || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))
|
||||
),
|
||||
};
|
||||
@@ -117,7 +147,7 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
|
||||
nextCipher.passwordHistory = await Promise.all(
|
||||
cipher.passwordHistory.map(async (entry) => ({
|
||||
...entry,
|
||||
decPassword: await decryptField(entry?.password || '', itemEnc, itemMac),
|
||||
decPassword: await decryptCipherField(entry?.password || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -125,36 +155,36 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
|
||||
if (cipher.card) {
|
||||
nextCipher.card = {
|
||||
...cipher.card,
|
||||
decCardholderName: await decryptField(cipher.card.cardholderName || '', itemEnc, itemMac),
|
||||
decNumber: await decryptField(cipher.card.number || '', itemEnc, itemMac),
|
||||
decBrand: await decryptField(cipher.card.brand || '', itemEnc, itemMac),
|
||||
decExpMonth: await decryptField(cipher.card.expMonth || '', itemEnc, itemMac),
|
||||
decExpYear: await decryptField(cipher.card.expYear || '', itemEnc, itemMac),
|
||||
decCode: await decryptField(cipher.card.code || '', itemEnc, itemMac),
|
||||
decCardholderName: await decryptCipherField(cipher.card.cardholderName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decNumber: await decryptCipherField(cipher.card.number || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decBrand: await decryptCipherField(cipher.card.brand || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decExpMonth: await decryptCipherField(cipher.card.expMonth || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decExpYear: await decryptCipherField(cipher.card.expYear || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCode: await decryptCipherField(cipher.card.code || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
if (cipher.identity) {
|
||||
nextCipher.identity = {
|
||||
...cipher.identity,
|
||||
decTitle: await decryptField(cipher.identity.title || '', itemEnc, itemMac),
|
||||
decFirstName: await decryptField(cipher.identity.firstName || '', itemEnc, itemMac),
|
||||
decMiddleName: await decryptField(cipher.identity.middleName || '', itemEnc, itemMac),
|
||||
decLastName: await decryptField(cipher.identity.lastName || '', itemEnc, itemMac),
|
||||
decUsername: await decryptField(cipher.identity.username || '', itemEnc, itemMac),
|
||||
decCompany: await decryptField(cipher.identity.company || '', itemEnc, itemMac),
|
||||
decSsn: await decryptField(cipher.identity.ssn || '', itemEnc, itemMac),
|
||||
decPassportNumber: await decryptField(cipher.identity.passportNumber || '', itemEnc, itemMac),
|
||||
decLicenseNumber: await decryptField(cipher.identity.licenseNumber || '', itemEnc, itemMac),
|
||||
decEmail: await decryptField(cipher.identity.email || '', itemEnc, itemMac),
|
||||
decPhone: await decryptField(cipher.identity.phone || '', itemEnc, itemMac),
|
||||
decAddress1: await decryptField(cipher.identity.address1 || '', itemEnc, itemMac),
|
||||
decAddress2: await decryptField(cipher.identity.address2 || '', itemEnc, itemMac),
|
||||
decAddress3: await decryptField(cipher.identity.address3 || '', itemEnc, itemMac),
|
||||
decCity: await decryptField(cipher.identity.city || '', itemEnc, itemMac),
|
||||
decState: await decryptField(cipher.identity.state || '', itemEnc, itemMac),
|
||||
decPostalCode: await decryptField(cipher.identity.postalCode || '', itemEnc, itemMac),
|
||||
decCountry: await decryptField(cipher.identity.country || '', itemEnc, itemMac),
|
||||
decTitle: await decryptCipherField(cipher.identity.title || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decFirstName: await decryptCipherField(cipher.identity.firstName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decMiddleName: await decryptCipherField(cipher.identity.middleName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decLastName: await decryptCipherField(cipher.identity.lastName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decUsername: await decryptCipherField(cipher.identity.username || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCompany: await decryptCipherField(cipher.identity.company || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decSsn: await decryptCipherField(cipher.identity.ssn || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPassportNumber: await decryptCipherField(cipher.identity.passportNumber || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decLicenseNumber: await decryptCipherField(cipher.identity.licenseNumber || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decEmail: await decryptCipherField(cipher.identity.email || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPhone: await decryptCipherField(cipher.identity.phone || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress1: await decryptCipherField(cipher.identity.address1 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress2: await decryptCipherField(cipher.identity.address2 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decAddress3: await decryptCipherField(cipher.identity.address3 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCity: await decryptCipherField(cipher.identity.city || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decState: await decryptCipherField(cipher.identity.state || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPostalCode: await decryptCipherField(cipher.identity.postalCode || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decCountry: await decryptCipherField(cipher.identity.country || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,11 +192,11 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
|
||||
const encryptedFingerprint = cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint || '';
|
||||
nextCipher.sshKey = {
|
||||
...cipher.sshKey,
|
||||
decPrivateKey: await decryptField(cipher.sshKey.privateKey || '', itemEnc, itemMac),
|
||||
decPublicKey: await decryptField(cipher.sshKey.publicKey || '', itemEnc, itemMac),
|
||||
decPrivateKey: await decryptCipherField(cipher.sshKey.privateKey || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decPublicKey: await decryptCipherField(cipher.sshKey.publicKey || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
keyFingerprint: encryptedFingerprint || null,
|
||||
fingerprint: encryptedFingerprint || null,
|
||||
decFingerprint: await decryptField(encryptedFingerprint, itemEnc, itemMac),
|
||||
decFingerprint: await decryptCipherField(encryptedFingerprint, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,8 +204,8 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise<Decr
|
||||
nextCipher.fields = await Promise.all(
|
||||
cipher.fields.map(async (field) => ({
|
||||
...field,
|
||||
decName: await decryptField(field.name || '', itemEnc, itemMac),
|
||||
decValue: await decryptField(field.value || '', itemEnc, itemMac),
|
||||
decName: await decryptCipherField(field.name || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
decValue: await decryptCipherField(field.value || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user