From 8df3221078d648d5f16a9a5f858185ffba02fb9e Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Wed, 4 Mar 2026 22:45:30 +0800 Subject: [PATCH] feat: add support for SSH key fingerprint normalization and compatibility --- package.json | 8 ++------ src/handlers/ciphers.ts | 26 +++++++++++++++++++++++++ src/handlers/import.ts | 4 ++-- webapp/src/App.tsx | 7 +++++-- webapp/src/components/VaultPage.tsx | 30 ++++++++++++++++++++++++++--- webapp/src/lib/api.ts | 10 ++++++++-- webapp/src/lib/export-formats.ts | 21 ++++++++++++++++++-- webapp/src/lib/types.ts | 1 + 8 files changed, 90 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 120cce2..91ff466 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,9 @@ "main": "src/index.ts", "type": "module", "scripts": { - "dev": "npm run web:build && wrangler dev -c wrangler.toml", - "dev:worker": "wrangler dev -c wrangler.toml", - "web:dev": "vite --config webapp/vite.config.ts", - "web:build": "vite build --config webapp/vite.config.ts", - "web:typecheck": "tsc -p webapp/tsconfig.json --noEmit", + "dev": "wrangler dev -c wrangler.toml", "build": "vite build --config webapp/vite.config.ts", - "deploy": "npm run build && wrangler deploy" + "deploy": "wrangler deploy" }, "keywords": [ "bitwarden", diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 992c836..dd5bd31 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -40,6 +40,28 @@ export function normalizeCipherLoginForCompatibility(login: any): any { }; } +// Android 2026.2.0 requires sshKey.keyFingerprint in sync payloads. +// Keep legacy alias "fingerprint" in parallel for older web payloads. +export function normalizeCipherSshKeyForCompatibility(sshKey: any): any { + if (!sshKey || typeof sshKey !== 'object') return sshKey ?? null; + + const candidate = + sshKey.keyFingerprint !== undefined && sshKey.keyFingerprint !== null + ? sshKey.keyFingerprint + : sshKey.fingerprint; + + const normalizedFingerprint = + candidate === undefined || candidate === null + ? '' + : String(candidate); + + return { + ...sshKey, + keyFingerprint: normalizedFingerprint, + fingerprint: normalizedFingerprint, + }; +} + // Format attachments for API response export function formatAttachments(attachments: Attachment[]): any[] | null { if (attachments.length === 0) return null; @@ -63,6 +85,7 @@ export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []) // Strip internal-only fields that must not appear in the API response const { userId, createdAt, updatedAt, deletedAt, ...passthrough } = cipher; const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null); + const normalizedSshKey = normalizeCipherSshKeyForCompatibility((passthrough as any).sshKey ?? null); return { // Pass through ALL stored cipher fields (known + unknown) @@ -85,6 +108,7 @@ export function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []) collectionIds: [], attachments: formatAttachments(attachments), login: normalizedLogin, + sshKey: normalizedSshKey, encryptedFor: null, }; } @@ -181,6 +205,7 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str deletedAt: null, }; cipher.login = normalizeCipherLoginForCompatibility(cipher.login); + cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey); const createFields = getAliasedProp(cipherData, ['fields', 'Fields']); cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null); @@ -232,6 +257,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str deletedAt: existingCipher.deletedAt, }; cipher.login = normalizeCipherLoginForCompatibility(cipher.login); + cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey); // Custom fields deletion compatibility: // - Accept both camelCase "fields" and PascalCase "Fields". diff --git a/src/handlers/import.ts b/src/handlers/import.ts index aa38963..11c7cfc 100644 --- a/src/handlers/import.ts +++ b/src/handlers/import.ts @@ -3,7 +3,7 @@ import { StorageService } from '../services/storage'; import { errorResponse, jsonResponse } from '../utils/response'; import { generateUUID } from '../utils/uuid'; import { LIMITS } from '../config/limits'; -import { normalizeCipherLoginForCompatibility } from './ciphers'; +import { normalizeCipherLoginForCompatibility, normalizeCipherSshKeyForCompatibility } from './ciphers'; // Bitwarden client import request format interface CiphersImportRequest { @@ -226,7 +226,7 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st })) || null, passwordHistory: c.passwordHistory ?? null, reprompt: c.reprompt ?? 0, - sshKey: (c as any).sshKey ?? null, + sshKey: normalizeCipherSshKeyForCompatibility((c as any).sshKey ?? null), key: (c as any).key ?? null, createdAt: now, updatedAt: now, diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 83bac97..cc15ad2 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -251,7 +251,7 @@ function importCipherToDraft(cipher: Record, folderId: string | const sshKey = (cipher.sshKey || {}) as Record; draft.sshPrivateKey = asText(sshKey.privateKey); draft.sshPublicKey = asText(sshKey.publicKey); - draft.sshFingerprint = asText(sshKey.fingerprint); + draft.sshFingerprint = asText(sshKey.keyFingerprint ?? sshKey.fingerprint); } return draft; @@ -703,11 +703,14 @@ export default function App() { }; } if (cipher.sshKey) { + 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), - decFingerprint: await decryptField(cipher.sshKey.fingerprint || '', itemEnc, itemMac), + keyFingerprint: encryptedFingerprint || null, + fingerprint: encryptedFingerprint || null, + decFingerprint: await decryptField(encryptedFingerprint, itemEnc, itemMac), }; } if (cipher.fields) { diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index 0917cb7..95087cc 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -1461,9 +1461,33 @@ function folderName(id: string | null | undefined): string { {selectedCipher.sshKey && (

{t('txt_ssh_key')}

-
{t('txt_private_key')}{maskSecret(selectedCipher.sshKey.decPrivateKey || '')}
-
{t('txt_public_key')}{selectedCipher.sshKey.decPublicKey || ''}
-
{t('txt_fingerprint')}{selectedCipher.sshKey.decFingerprint || ''}
+
+ {t('txt_private_key')} +
+ + {maskSecret(selectedCipher.sshKey.decPrivateKey || '')} + +
+
+
+
+ {t('txt_public_key')} +
+ + {selectedCipher.sshKey.decPublicKey || ''} + +
+
+
+
+ {t('txt_fingerprint')} +
+ + {selectedCipher.sshKey.decFingerprint || ''} + +
+
+
)} diff --git a/webapp/src/lib/api.ts b/webapp/src/lib/api.ts index 1aabd79..c6fd0ed 100644 --- a/webapp/src/lib/api.ts +++ b/webapp/src/lib/api.ts @@ -971,10 +971,13 @@ export async function createCipher( country: await encryptTextValue(draft.identCountry, enc, mac), }; } else if (type === 5) { + const encryptedFingerprint = await encryptTextValue(draft.sshFingerprint, enc, mac); payload.sshKey = { privateKey: await encryptTextValue(draft.sshPrivateKey, enc, mac), publicKey: await encryptTextValue(draft.sshPublicKey, enc, mac), - fingerprint: await encryptTextValue(draft.sshFingerprint, enc, mac), + keyFingerprint: encryptedFingerprint, + // Keep legacy alias for backward compatibility with previously exported/edited items. + fingerprint: encryptedFingerprint, }; } else if (type === 2) { payload.secureNote = { type: 0 }; @@ -1063,10 +1066,13 @@ export async function updateCipher( country: await encryptTextValue(draft.identCountry, keys.enc, keys.mac), }; } else if (type === 5) { + const encryptedFingerprint = await encryptTextValue(draft.sshFingerprint, keys.enc, keys.mac); payload.sshKey = { privateKey: await encryptTextValue(draft.sshPrivateKey, keys.enc, keys.mac), publicKey: await encryptTextValue(draft.sshPublicKey, keys.enc, keys.mac), - fingerprint: await encryptTextValue(draft.sshFingerprint, keys.enc, keys.mac), + keyFingerprint: encryptedFingerprint, + // Keep legacy alias for backward compatibility with previously exported/edited items. + fingerprint: encryptedFingerprint, }; } else if (type === 2) { payload.secureNote = { type: 0 }; diff --git a/webapp/src/lib/export-formats.ts b/webapp/src/lib/export-formats.ts index 97f41f7..6863c85 100644 --- a/webapp/src/lib/export-formats.ts +++ b/webapp/src/lib/export-formats.ts @@ -257,7 +257,9 @@ function mapCipherEncrypted(cipher: Cipher): Record { ? { privateKey: cipher.sshKey.privateKey ?? null, publicKey: cipher.sshKey.publicKey ?? null, - fingerprint: cipher.sshKey.fingerprint ?? null, + keyFingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, + // Keep legacy alias for compatibility with older importers. + fingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, } : null; @@ -304,7 +306,22 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint 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.sshKey = cipher.sshKey ? await deepDecryptUnknown(cipher.sshKey, keyParts.enc, keyParts.mac) : null; + if (cipher.sshKey) { + const fingerprint = await decryptMaybe( + cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, + keyParts.enc, + keyParts.mac + ); + out.sshKey = { + privateKey: await decryptMaybe(cipher.sshKey.privateKey ?? null, keyParts.enc, keyParts.mac), + publicKey: await decryptMaybe(cipher.sshKey.publicKey ?? null, keyParts.enc, keyParts.mac), + keyFingerprint: fingerprint, + // Keep legacy alias for compatibility with older importers. + fingerprint, + }; + } else { + out.sshKey = null; + } out.secureNote = cipher.secureNote ? { type: normalizeNumber((cipher.secureNote as { type?: unknown }).type, 0), diff --git a/webapp/src/lib/types.ts b/webapp/src/lib/types.ts index 647075b..102e367 100644 --- a/webapp/src/lib/types.ts +++ b/webapp/src/lib/types.ts @@ -112,6 +112,7 @@ export interface CipherIdentity { export interface CipherSshKey { privateKey?: string | null; publicKey?: string | null; + keyFingerprint?: string | null; fingerprint?: string | null; decPrivateKey?: string; decPublicKey?: string;