|
| 1 | +/** |
| 2 | + * v3 read-path pin. v3 reuses the EncryptedString envelope and the v2 decrypt |
| 3 | + * path verbatim: a v3 STORED payload `{v, i:{t,c}, c}` is exactly the protect-ffi |
| 4 | + * Encrypted shape, so it passes `isEncryptedPayload` / `ensureEncryptedEnvelope` |
| 5 | + * unchanged, and `decryptAll` / `EncryptedString#decrypt()` are version-agnostic. |
| 6 | + * No decrypt code change was needed (Round-3 §B1) — this test pins it as a |
| 7 | + * regression. |
| 8 | + */ |
| 9 | +import { isEncryptedPayload } from '@cipherstash/stack' |
| 10 | +import { describe, expect, it, vi } from 'vitest' |
| 11 | +import { createCipherstashStringV3Codec } from '../../src/execution/codec-v3' |
| 12 | +import { decryptAll } from '../../src/execution/decrypt-all' |
| 13 | +import { EncryptedString } from '../../src/execution/envelope-string' |
| 14 | +import { makeFakeSdk } from './helpers/fake-sdk' |
| 15 | + |
| 16 | +const ctx = (table: string, name: string) => ({ column: { table, name } }) as never |
| 17 | + |
| 18 | +describe('decryptAll over v3 envelopes', () => { |
| 19 | + it('groups v3 read-side envelopes by (table,column) and bulk-decrypts once per group', async () => { |
| 20 | + const bulkDecrypt = vi.fn(makeFakeSdk().bulkDecrypt) |
| 21 | + const sdk = makeFakeSdk({ bulkDecrypt }) |
| 22 | + const codec = createCipherstashStringV3Codec(sdk) |
| 23 | + const env = await codec.decode('{"v":2,"i":{"t":"users","c":"email"},"c":"ct"}', ctx('users', 'email')) |
| 24 | + const rows = [{ email: env }] |
| 25 | + |
| 26 | + await decryptAll(rows) |
| 27 | + |
| 28 | + expect(bulkDecrypt).toHaveBeenCalledOnce() |
| 29 | + expect(await (rows[0]!.email as EncryptedString).decrypt()).toBe('plaintext') |
| 30 | + }) |
| 31 | + |
| 32 | + it('a v3 stored payload satisfies isEncryptedPayload (the v2 read-path gate)', () => { |
| 33 | + // A v3 STORED value is a full payload; the stack helper that the v2 decrypt |
| 34 | + // path uses must accept it unchanged. |
| 35 | + expect(isEncryptedPayload({ v: 2, i: { t: 'users', c: 'email' }, c: 'ct' })).toBe(true) |
| 36 | + // A v3 SEARCH term (no ciphertext `c`) is NOT a stored value and must never be |
| 37 | + // routed into decrypt; it correctly fails the stored-payload gate. |
| 38 | + expect(isEncryptedPayload({ v: 2, i: { t: 'users', c: 'email' }, hm: 'h' })).toBe(false) |
| 39 | + }) |
| 40 | +}) |
0 commit comments