From 783fcbbe4bb4ddf18dad78ee05191ba794385ffb Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sat, 28 Mar 2026 01:18:40 +0800 Subject: [PATCH] feat: add normalization functions for optional IDs and public keys in cipher and user decryption handling --- src/handlers/ciphers.ts | 22 ++++++++++++++++------ src/services/storage-cipher-repo.ts | 22 +++++++++++++++++----- src/utils/user-decryption.ts | 11 +++++++++-- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index a934a65..f1f9490 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -7,6 +7,12 @@ import { deleteAllAttachmentsForCipher } from './attachments'; import { parsePagination, encodeContinuationToken } from '../utils/pagination'; import { readActingDeviceIdentifier } from '../utils/device'; +function normalizeOptionalId(value: unknown): string | null { + if (value == null) return null; + const normalized = String(value).trim(); + return normalized ? normalized : null; +} + async function notifyVaultSyncForRequest( request: Request, env: Env, @@ -47,6 +53,7 @@ function syncCipherComputedAliases(cipher: Cipher): Cipher { function normalizeCipherForStorage(cipher: Cipher): Cipher { cipher.login = normalizeCipherLoginForStorage(cipher.login); cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey); + cipher.folderId = normalizeOptionalId(cipher.folderId); const hasArchivedAt = Object.prototype.hasOwnProperty.call(cipher as object, 'archivedAt'); cipher.archivedAt = hasArchivedAt ? normalizeCipherTimestamp(cipher.archivedAt) ?? null @@ -185,6 +192,7 @@ export function cipherToResponse( // Pass through ALL stored cipher fields (known + unknown) ...passthrough, // Server-computed / enforced fields (always override) + folderId: normalizeOptionalId(cipher.folderId), type: Number(cipher.type) || 1, organizationId: null, organizationUseTotp: false, @@ -499,11 +507,12 @@ export async function handlePartialUpdateCipher(request: Request, env: Env, user } if (body.folderId !== undefined) { - if (body.folderId) { - const folderOk = await verifyFolderOwnership(storage, body.folderId, userId); + const folderId = normalizeOptionalId(body.folderId); + if (folderId) { + const folderOk = await verifyFolderOwnership(storage, folderId, userId); if (!folderOk) return errorResponse('Folder not found', 404); } - cipher.folderId = body.folderId; + cipher.folderId = folderId; } if (body.favorite !== undefined) { cipher.favorite = body.favorite; @@ -537,12 +546,13 @@ export async function handleBulkMoveCiphers(request: Request, env: Env, userId: return errorResponse('ids array is required', 400); } - if (body.folderId) { - const folderOk = await verifyFolderOwnership(storage, body.folderId, userId); + const folderId = normalizeOptionalId(body.folderId); + if (folderId) { + const folderOk = await verifyFolderOwnership(storage, folderId, userId); if (!folderOk) return errorResponse('Folder not found', 404); } - const revisionDate = await storage.bulkMoveCiphers(body.ids, body.folderId || null, userId); + const revisionDate = await storage.bulkMoveCiphers(body.ids, folderId, userId); if (revisionDate) { await notifyVaultSyncForRequest(request, env, userId, revisionDate); } diff --git a/src/services/storage-cipher-repo.ts b/src/services/storage-cipher-repo.ts index 3f5fd26..ba6621f 100644 --- a/src/services/storage-cipher-repo.ts +++ b/src/services/storage-cipher-repo.ts @@ -1,5 +1,11 @@ import type { Cipher } from '../types'; +function normalizeOptionalId(value: unknown): string | null { + if (value == null) return null; + const normalized = String(value).trim(); + return normalized ? normalized : null; +} + type SafeBind = (stmt: D1PreparedStatement, ...values: any[]) => D1PreparedStatement; type SqlChunkSize = (fixedBindCount: number) => number; type UpdateRevisionDate = (userId: string) => Promise; @@ -25,12 +31,13 @@ function parseCipherRow(row: CipherRow | null | undefined): Cipher | null { if (!row?.data) return null; try { const parsed = JSON.parse(row.data) as Cipher; + const folderId = normalizeOptionalId(row.folder_id ?? parsed.folderId ?? null); return { ...parsed, id: row.id, userId: row.user_id, type: Number(row.type) || Number(parsed.type) || 1, - folderId: row.folder_id ?? parsed.folderId ?? null, + folderId, name: row.name ?? parsed.name ?? null, notes: row.notes ?? parsed.notes ?? null, favorite: row.favorite != null ? !!row.favorite : !!parsed.favorite, @@ -60,7 +67,11 @@ export async function getCipher(db: D1Database, id: string): Promise { - const data = JSON.stringify(cipher); + const folderId = normalizeOptionalId(cipher.folderId); + const data = JSON.stringify({ + ...cipher, + folderId, + }); const stmt = db.prepare( 'INSERT INTO ciphers(id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at) ' + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' + @@ -72,7 +83,7 @@ export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cip cipher.id, cipher.userId, Number(cipher.type) || 1, - cipher.folderId, + folderId, cipher.name, cipher.notes, cipher.favorite ? 1 : 0, @@ -249,8 +260,9 @@ export async function bulkMoveCiphers( ): Promise { if (ids.length === 0) return null; const now = new Date().toISOString(); + const normalizedFolderId = normalizeOptionalId(folderId); const uniqueIds = sanitizeIds(ids); - const patch = JSON.stringify({ folderId, updatedAt: now }); + const patch = JSON.stringify({ folderId: normalizedFolderId, updatedAt: now }); const chunkSize = sqlChunkSize(4); for (let i = 0; i < uniqueIds.length; i += chunkSize) { @@ -262,7 +274,7 @@ export async function bulkMoveCiphers( SET folder_id = ?, updated_at = ?, data = json_patch(data, ?) WHERE user_id = ? AND id IN (${placeholders})` ) - .bind(folderId, now, patch, userId, ...chunk) + .bind(normalizedFolderId, now, patch, userId, ...chunk) .run(); } diff --git a/src/utils/user-decryption.ts b/src/utils/user-decryption.ts index ed75d14..5baf9f6 100644 --- a/src/utils/user-decryption.ts +++ b/src/utils/user-decryption.ts @@ -1,14 +1,21 @@ import { User, UserDecryptionOptions } from '../types'; +function normalizeOptionalPublicKey(value: unknown): string { + if (value == null) return ''; + return String(value); +} + export function buildAccountKeys(user: Pick): Record | null { - if (!user.privateKey || !user.publicKey) { + if (!user.privateKey) { return null; } + const publicKey = normalizeOptionalPublicKey(user.publicKey); + return { publicKeyEncryptionKeyPair: { wrappedPrivateKey: user.privateKey, - publicKey: user.publicKey, + publicKey, Object: 'publicKeyEncryptionKeyPair', }, Object: 'privateKeys',