From f80aa2dcfc5af8f2ce290ddd23b86734dcef929a Mon Sep 17 00:00:00 2001 From: Harvey Zuccon Date: Wed, 25 Feb 2026 14:14:59 +1100 Subject: [PATCH] Speedup encryption storage --- src/storage/encrypted-storage.ts | 82 +++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/storage/encrypted-storage.ts b/src/storage/encrypted-storage.ts index 9d50944..7bb9bcc 100644 --- a/src/storage/encrypted-storage.ts +++ b/src/storage/encrypted-storage.ts @@ -19,6 +19,20 @@ export class EncryptedStorage< structuredClone: true }); + /** + * Memoization cache for deterministic encryption. + * Maps a packed+base64 plaintext representation to its encrypted base64 + * ciphertext. Safe because deterministic encryption always produces the + * same output for the same input and key. + */ + private readonly encryptCache = new Map(); + + /** + * Memoization cache for decryption. + * Maps an encrypted base64 ciphertext back to its unpacked plaintext value. + */ + private readonly decryptCache = new Map(); + constructor( private readonly storage: BaseStorage>, private readonly key: AESKey, @@ -56,10 +70,10 @@ export class EncryptedStorage< } async insertMany(documents: Array): Promise { - const encrypted = []; - for (const document of documents) { - encrypted.push(await this.convertToEncrypted(document)); - } + // Encrypt all documents in parallel. + const encrypted = await Promise.all( + documents.map((doc) => this.convertToEncrypted(doc)), + ); await this.storage.insertMany(encrypted); } @@ -96,48 +110,58 @@ export class EncryptedStorage< private async convertToEncrypted( document: Partial, ): Promise> { - // For each key in the document, encrypt the value. This requires us to know the type of each value, so we must include it after converting it. Maybe this can be done by converting it to an object and json stringifying it. - // Example: { a: 1, b: 'hello' } -> { a: { type: 'number', value: 1 }, b: { type: 'string', value: 'hello' } } const encrypted: Record = {}; - const formattedDocument = this.formatDocumentForEncryption(document); + const entries = Object.entries(formattedDocument); - for (const [key, value] of Object.entries(formattedDocument)) { - // Create our object to encrypt - const bin = this.msgpackr.pack(value); + // Encrypt all fields in parallel, using the cache when possible. + const results = await Promise.all( + entries.map(async ([key, value]) => { + const bin = this.msgpackr.pack(value); + const cacheKey = Bytes.from(bin).toBase64(); - // Encrypt it - const encryptedValue = await this.key.encrypt(bin, true); + let ciphertext = this.encryptCache.get(cacheKey); + if (ciphertext === undefined) { + const encryptedValue = await this.key.encrypt(bin, true); + ciphertext = encryptedValue.toBase64(); + this.encryptCache.set(cacheKey, ciphertext); + } - // Store the encrypted value in the encrypted object. - encrypted[key] = encryptedValue.toBase64(); + return [key, ciphertext] as const; + }), + ); + + for (const [key, ciphertext] of results) { + encrypted[key] = ciphertext; } - // Return the encrypted object. return encrypted; } private async convertToDecrypted( document: Record, ): Promise { + const entries = Object.entries(document); + + // Decrypt all fields in parallel, using the cache when possible. + const results = await Promise.all( + entries.map(async ([key, ciphertext]) => { + let value = this.decryptCache.get(ciphertext); + if (value === undefined) { + const bin = await this.key.decrypt(Bytes.fromBase64(ciphertext)); + value = this.msgpackr.unpack(bin); + this.decryptCache.set(ciphertext, value); + } + return [key, value] as const; + }), + ); + const decrypted: Record = {}; - - // Iterate through each key and value in the document and decrypt it. - for (const [key, value] of Object.entries(document)) { - // Decrypt the value. - const binaryString = await this.key.decrypt(Bytes.fromBase64(value)); - - // Unpack the value. - const object = this.msgpackr.unpack(binaryString); - - // Decode the value. - decrypted[key] = object; + for (const [key, value] of results) { + decrypted[key] = value; } - // Format the document from decryption. const decodedDocument = this.formatDocumentFromDecryption(decrypted); - - // Return the document as the original type. return decodedDocument as T; }