feat: add normalization functions for optional IDs and public keys in cipher and user decryption handling

This commit is contained in:
shuaiplus
2026-03-28 01:18:40 +08:00
parent 9e892e85a2
commit 783fcbbe4b
3 changed files with 42 additions and 13 deletions
+16 -6
View File
@@ -7,6 +7,12 @@ import { deleteAllAttachmentsForCipher } from './attachments';
import { parsePagination, encodeContinuationToken } from '../utils/pagination'; import { parsePagination, encodeContinuationToken } from '../utils/pagination';
import { readActingDeviceIdentifier } from '../utils/device'; 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( async function notifyVaultSyncForRequest(
request: Request, request: Request,
env: Env, env: Env,
@@ -47,6 +53,7 @@ function syncCipherComputedAliases(cipher: Cipher): Cipher {
function normalizeCipherForStorage(cipher: Cipher): Cipher { function normalizeCipherForStorage(cipher: Cipher): Cipher {
cipher.login = normalizeCipherLoginForStorage(cipher.login); cipher.login = normalizeCipherLoginForStorage(cipher.login);
cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey); cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey);
cipher.folderId = normalizeOptionalId(cipher.folderId);
const hasArchivedAt = Object.prototype.hasOwnProperty.call(cipher as object, 'archivedAt'); const hasArchivedAt = Object.prototype.hasOwnProperty.call(cipher as object, 'archivedAt');
cipher.archivedAt = hasArchivedAt cipher.archivedAt = hasArchivedAt
? normalizeCipherTimestamp(cipher.archivedAt) ?? null ? normalizeCipherTimestamp(cipher.archivedAt) ?? null
@@ -185,6 +192,7 @@ export function cipherToResponse(
// Pass through ALL stored cipher fields (known + unknown) // Pass through ALL stored cipher fields (known + unknown)
...passthrough, ...passthrough,
// Server-computed / enforced fields (always override) // Server-computed / enforced fields (always override)
folderId: normalizeOptionalId(cipher.folderId),
type: Number(cipher.type) || 1, type: Number(cipher.type) || 1,
organizationId: null, organizationId: null,
organizationUseTotp: false, organizationUseTotp: false,
@@ -499,11 +507,12 @@ export async function handlePartialUpdateCipher(request: Request, env: Env, user
} }
if (body.folderId !== undefined) { if (body.folderId !== undefined) {
if (body.folderId) { const folderId = normalizeOptionalId(body.folderId);
const folderOk = await verifyFolderOwnership(storage, body.folderId, userId); if (folderId) {
const folderOk = await verifyFolderOwnership(storage, folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404); if (!folderOk) return errorResponse('Folder not found', 404);
} }
cipher.folderId = body.folderId; cipher.folderId = folderId;
} }
if (body.favorite !== undefined) { if (body.favorite !== undefined) {
cipher.favorite = body.favorite; cipher.favorite = body.favorite;
@@ -537,12 +546,13 @@ export async function handleBulkMoveCiphers(request: Request, env: Env, userId:
return errorResponse('ids array is required', 400); return errorResponse('ids array is required', 400);
} }
if (body.folderId) { const folderId = normalizeOptionalId(body.folderId);
const folderOk = await verifyFolderOwnership(storage, body.folderId, userId); if (folderId) {
const folderOk = await verifyFolderOwnership(storage, folderId, userId);
if (!folderOk) return errorResponse('Folder not found', 404); 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) { if (revisionDate) {
await notifyVaultSyncForRequest(request, env, userId, revisionDate); await notifyVaultSyncForRequest(request, env, userId, revisionDate);
} }
+17 -5
View File
@@ -1,5 +1,11 @@
import type { Cipher } from '../types'; 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 SafeBind = (stmt: D1PreparedStatement, ...values: any[]) => D1PreparedStatement;
type SqlChunkSize = (fixedBindCount: number) => number; type SqlChunkSize = (fixedBindCount: number) => number;
type UpdateRevisionDate = (userId: string) => Promise<string>; type UpdateRevisionDate = (userId: string) => Promise<string>;
@@ -25,12 +31,13 @@ function parseCipherRow(row: CipherRow | null | undefined): Cipher | null {
if (!row?.data) return null; if (!row?.data) return null;
try { try {
const parsed = JSON.parse(row.data) as Cipher; const parsed = JSON.parse(row.data) as Cipher;
const folderId = normalizeOptionalId(row.folder_id ?? parsed.folderId ?? null);
return { return {
...parsed, ...parsed,
id: row.id, id: row.id,
userId: row.user_id, userId: row.user_id,
type: Number(row.type) || Number(parsed.type) || 1, type: Number(row.type) || Number(parsed.type) || 1,
folderId: row.folder_id ?? parsed.folderId ?? null, folderId,
name: row.name ?? parsed.name ?? null, name: row.name ?? parsed.name ?? null,
notes: row.notes ?? parsed.notes ?? null, notes: row.notes ?? parsed.notes ?? null,
favorite: row.favorite != null ? !!row.favorite : !!parsed.favorite, favorite: row.favorite != null ? !!row.favorite : !!parsed.favorite,
@@ -60,7 +67,11 @@ export async function getCipher(db: D1Database, id: string): Promise<Cipher | nu
} }
export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cipher): Promise<void> { export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cipher): Promise<void> {
const data = JSON.stringify(cipher); const folderId = normalizeOptionalId(cipher.folderId);
const data = JSON.stringify({
...cipher,
folderId,
});
const stmt = db.prepare( 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) ' + 'INSERT INTO ciphers(id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at) ' +
'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' + 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' +
@@ -72,7 +83,7 @@ export async function saveCipher(db: D1Database, safeBind: SafeBind, cipher: Cip
cipher.id, cipher.id,
cipher.userId, cipher.userId,
Number(cipher.type) || 1, Number(cipher.type) || 1,
cipher.folderId, folderId,
cipher.name, cipher.name,
cipher.notes, cipher.notes,
cipher.favorite ? 1 : 0, cipher.favorite ? 1 : 0,
@@ -249,8 +260,9 @@ export async function bulkMoveCiphers(
): Promise<string | null> { ): Promise<string | null> {
if (ids.length === 0) return null; if (ids.length === 0) return null;
const now = new Date().toISOString(); const now = new Date().toISOString();
const normalizedFolderId = normalizeOptionalId(folderId);
const uniqueIds = sanitizeIds(ids); const uniqueIds = sanitizeIds(ids);
const patch = JSON.stringify({ folderId, updatedAt: now }); const patch = JSON.stringify({ folderId: normalizedFolderId, updatedAt: now });
const chunkSize = sqlChunkSize(4); const chunkSize = sqlChunkSize(4);
for (let i = 0; i < uniqueIds.length; i += chunkSize) { 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, ?) SET folder_id = ?, updated_at = ?, data = json_patch(data, ?)
WHERE user_id = ? AND id IN (${placeholders})` WHERE user_id = ? AND id IN (${placeholders})`
) )
.bind(folderId, now, patch, userId, ...chunk) .bind(normalizedFolderId, now, patch, userId, ...chunk)
.run(); .run();
} }
+9 -2
View File
@@ -1,14 +1,21 @@
import { User, UserDecryptionOptions } from '../types'; import { User, UserDecryptionOptions } from '../types';
function normalizeOptionalPublicKey(value: unknown): string {
if (value == null) return '';
return String(value);
}
export function buildAccountKeys(user: Pick<User, 'privateKey' | 'publicKey'>): Record<string, unknown> | null { export function buildAccountKeys(user: Pick<User, 'privateKey' | 'publicKey'>): Record<string, unknown> | null {
if (!user.privateKey || !user.publicKey) { if (!user.privateKey) {
return null; return null;
} }
const publicKey = normalizeOptionalPublicKey(user.publicKey);
return { return {
publicKeyEncryptionKeyPair: { publicKeyEncryptionKeyPair: {
wrappedPrivateKey: user.privateKey, wrappedPrivateKey: user.privateKey,
publicKey: user.publicKey, publicKey,
Object: 'publicKeyEncryptionKeyPair', Object: 'publicKeyEncryptionKeyPair',
}, },
Object: 'privateKeys', Object: 'privateKeys',