fix(storage-encrypted): tilfeldig AES-GCM-nonce + vault-nøkkelgren
To endringer i samme krypto-filer.
G0-P0: deriveNonce() lagde nonce som en ren funksjon av radens identitet.
saveSession re-forsegler ved hvert ratchet-steg, så (key, nonce) gjentok seg
per konstruksjon over ULIK plaintext — GCM forbidden attack, som lekker både
XOR-en av plaintekstene og GHASH-subnøkkelen. Fire kallesteder: sealString og
sealBytes tar nå randomNonce(), openString/openBytes slutter å tvinge den.
Ingen migrering: aeadOpen har alltid lest nonce fra blob-prefikset, så
eksisterende data åpnes uendret. Testet eksplisitt. deriveNonce og
expectedNonce beholdt som deprecated — begge er del av den publiserte flaten.
Android var aldri rammet; KeystoreStorage bruker cipher.iv, som Android
Keystore alltid genererer tilfeldig.
V4.13: egen HKDF-gren for vault (shade-vault-{id,content,sig}-v1). Ikke
gjenbruk av shade-blob-*-v1: en vault-nøkkel leser hver eneste fil, en
profil-blob-nøkkel leser en vertsliste. Delt derivasjon ville gjort ett
kompromiss om til det andre.
9 nye tester; testene er bevist å ha verdi ved å reversere fiksen —
nøyaktig de tre som beskriver feilen faller. 87 grønne totalt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,12 +34,16 @@ export {
|
|||||||
deriveBlobSlotId,
|
deriveBlobSlotId,
|
||||||
deriveBlobKey,
|
deriveBlobKey,
|
||||||
deriveBlobSigningSeed,
|
deriveBlobSigningSeed,
|
||||||
|
deriveVaultId,
|
||||||
|
deriveVaultContentKey,
|
||||||
|
deriveVaultSigningSeed,
|
||||||
} from './crypto/kdf.js';
|
} from './crypto/kdf.js';
|
||||||
export {
|
export {
|
||||||
AEAD_NONCE_LEN,
|
AEAD_NONCE_LEN,
|
||||||
AEAD_TAG_LEN,
|
AEAD_TAG_LEN,
|
||||||
aeadSeal,
|
aeadSeal,
|
||||||
aeadOpen,
|
aeadOpen,
|
||||||
|
randomNonce,
|
||||||
} from './crypto/aead.js';
|
} from './crypto/aead.js';
|
||||||
export {
|
export {
|
||||||
COL,
|
COL,
|
||||||
|
|||||||
@@ -33,9 +33,32 @@ async function importKey(key: Uint8Array, usages: WebCryptoKeyUsage[]): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt a plaintext blob with the given key and a deterministic nonce.
|
* A fresh random 12-byte nonce.
|
||||||
* Returns `nonce || ct||tag` as a single Uint8Array suitable for direct
|
*
|
||||||
* BLOB storage.
|
* AES-GCM tolerates key reuse but not (key, nonce) reuse: two ciphertexts
|
||||||
|
* sealed under the same pair leak the XOR of their plaintexts and, worse,
|
||||||
|
* the GHASH authentication subkey — the "forbidden attack", which yields
|
||||||
|
* tag forgery for that key. Mutable rows are re-sealed on every change
|
||||||
|
* (`saveSession` runs on each ratchet step), so a nonce derived from row
|
||||||
|
* identity alone repeats by construction.
|
||||||
|
*
|
||||||
|
* Random beats a counter here because the codec is shared across SQLite
|
||||||
|
* and Postgres and has no durable per-row write counter to lean on. At
|
||||||
|
* 96 bits, collision probability stays negligible far past any realistic
|
||||||
|
* number of re-saves.
|
||||||
|
*/
|
||||||
|
export function randomNonce(): Uint8Array {
|
||||||
|
return globalThis.crypto.getRandomValues(new Uint8Array(NONCE_LEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a plaintext blob with the given key and nonce. Returns
|
||||||
|
* `nonce || ct||tag` as a single Uint8Array suitable for direct BLOB
|
||||||
|
* storage.
|
||||||
|
*
|
||||||
|
* Callers must pass a nonce that is fresh for this key — see
|
||||||
|
* {@link randomNonce}. Never derive one deterministically from row
|
||||||
|
* identity for a row that can be written more than once.
|
||||||
*/
|
*/
|
||||||
export async function aeadSeal(
|
export async function aeadSeal(
|
||||||
key: Uint8Array,
|
key: Uint8Array,
|
||||||
@@ -58,9 +81,19 @@ export async function aeadSeal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a `nonce || ct||tag` blob. The expected nonce is verified against
|
* Decrypt a `nonce || ct||tag` blob.
|
||||||
* the prefix to detect tampering before we even reach the AEAD; if the
|
*
|
||||||
* caller passes a `expectedNonce`, mismatch throws before SubtleCrypto runs.
|
* The nonce always comes from the blob prefix, which is what makes the
|
||||||
|
* move to random nonces backward compatible: a blob sealed under the old
|
||||||
|
* deterministic scheme opens unchanged, because its nonce was already
|
||||||
|
* stored the same way.
|
||||||
|
*
|
||||||
|
* @param expectedNonce **Deprecated.** Only meaningful when the nonce is
|
||||||
|
* a pure function of row identity, which is exactly the pattern that made
|
||||||
|
* (key, nonce) repeat. It adds no tamper detection that the AEAD tag and
|
||||||
|
* the (table, column, pk) AAD do not already provide — a flipped bit in
|
||||||
|
* the nonce fails the tag. Retained so existing callers keep compiling;
|
||||||
|
* pass nothing.
|
||||||
*/
|
*/
|
||||||
export async function aeadOpen(
|
export async function aeadOpen(
|
||||||
key: Uint8Array,
|
key: Uint8Array,
|
||||||
|
|||||||
@@ -106,12 +106,20 @@ export function deriveFieldKey(storageKey: Uint8Array, table: string, column: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive a deterministic 12-byte AEAD nonce from a row key (typically the
|
* Derive a deterministic 12-byte AEAD nonce from a row key plus (table, pk).
|
||||||
* field key) plus (table, pk) binding. With per-field keys, deterministic
|
*
|
||||||
* nonces are safe because each (key, plaintext) pair appears at most once
|
* @deprecated Unsafe for anything that can be written twice, which is every
|
||||||
* — re-saving the same row reuses the (nonce, key) pair only because the
|
* mutable row. The original rationale held that re-saving a row was fine
|
||||||
* plaintext also changes (chain ratchet, prekey state, etc.). The AAD
|
* "because the plaintext also changes" — but that is precisely the case
|
||||||
* also binds (table, column, pk) so swapping is rejected on decrypt.
|
* AES-GCM forbids: reusing (key, nonce) across *different* plaintexts leaks
|
||||||
|
* their XOR and the GHASH subkey (the forbidden attack), enabling tag
|
||||||
|
* forgery. `saveSession` re-seals on every ratchet step, so the pair
|
||||||
|
* repeated by construction. Use {@link randomNonce} from `./aead.js`.
|
||||||
|
*
|
||||||
|
* Kept exported (it is part of the published surface via `index.ts` and
|
||||||
|
* `crypto.ts`) so consumers keep compiling. No caller inside this package
|
||||||
|
* uses it any more. Reading old data needs no migration: `aeadOpen` has
|
||||||
|
* always taken the nonce from the blob prefix.
|
||||||
*/
|
*/
|
||||||
export function deriveNonce(rowKey: Uint8Array, table: string, pk: string): Uint8Array {
|
export function deriveNonce(rowKey: Uint8Array, table: string, pk: string): Uint8Array {
|
||||||
const out = hkdfDerive(rowKey, `shade-row-nonce-v1:${table}:${pk}`, 12);
|
const out = hkdfDerive(rowKey, `shade-row-nonce-v1:${table}:${pk}`, 12);
|
||||||
@@ -161,3 +169,42 @@ export function deriveBlobKey(masterKey: Uint8Array, app: string): Uint8Array {
|
|||||||
export function deriveBlobSigningSeed(masterKey: Uint8Array, app: string): Uint8Array {
|
export function deriveBlobSigningSeed(masterKey: Uint8Array, app: string): Uint8Array {
|
||||||
return hkdfDerive(masterKey, `shade-blob-sig-v1:${app}`, 32);
|
return hkdfDerive(masterKey, `shade-blob-sig-v1:${app}`, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── V4.13 — vault (server-side encrypted file store) ──────────
|
||||||
|
//
|
||||||
|
// The blob primitive above holds ONE small blob per slot. A vault holds
|
||||||
|
// a whole collection — a project workspace, a mailbox, a drive folder —
|
||||||
|
// as content-addressed objects plus an append-only log of manifests.
|
||||||
|
//
|
||||||
|
// Its own HKDF branch rather than a reuse of `shade-blob-*-v1`, for two
|
||||||
|
// reasons. A vault key is used on far more ciphertext than a profile
|
||||||
|
// blob ever is, and the two have different blast radii: whoever holds
|
||||||
|
// the vault key can read every file, while the profile key exposes only
|
||||||
|
// the host list. Separate labels mean compromising one cannot be turned
|
||||||
|
// into the other, even though both hang off the same master.
|
||||||
|
//
|
||||||
|
// vaultId = HKDF(masterKey, info=`shade-vault-id-v1:${app}`)
|
||||||
|
// contentKey = HKDF(masterKey, info=`shade-vault-content-v1:${app}`)
|
||||||
|
// sigSeed = HKDF(masterKey, info=`shade-vault-sig-v1:${app}`)
|
||||||
|
|
||||||
|
/** 32-byte vault identifier. Opaque to the relay, like a slotId. */
|
||||||
|
export function deriveVaultId(masterKey: Uint8Array, app: string): Uint8Array {
|
||||||
|
return hkdfDerive(masterKey, `shade-vault-id-v1:${app}`, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AEAD key for the vault's objects and manifests.
|
||||||
|
*
|
||||||
|
* One key for the whole collection, not one per file: per-file keys would
|
||||||
|
* have to be stored somewhere, and that somewhere would be a manifest
|
||||||
|
* encrypted under a collection key anyway. Object AAD binds the content
|
||||||
|
* hash, so a ciphertext cannot be moved to another object's name.
|
||||||
|
*/
|
||||||
|
export function deriveVaultContentKey(masterKey: Uint8Array, app: string): Uint8Array {
|
||||||
|
return hkdfDerive(masterKey, `shade-vault-content-v1:${app}`, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 32-byte Ed25519 signing seed; TOFU-pinned by the relay on first write. */
|
||||||
|
export function deriveVaultSigningSeed(masterKey: Uint8Array, app: string): Uint8Array {
|
||||||
|
return hkdfDerive(masterKey, `shade-vault-sig-v1:${app}`, 32);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
serializeSignedPreKey, deserializeSignedPreKey,
|
serializeSignedPreKey, deserializeSignedPreKey,
|
||||||
toBase64, fromBase64,
|
toBase64, fromBase64,
|
||||||
} from '@shade/core';
|
} from '@shade/core';
|
||||||
import { aeadOpen, aeadSeal } from './aead.js';
|
import { aeadOpen, aeadSeal, randomNonce } from './aead.js';
|
||||||
import { buildAad, deriveNonce } from './kdf.js';
|
import { buildAad } from './kdf.js';
|
||||||
import type { KeyManager } from './key-manager.js';
|
import type { KeyManager } from './key-manager.js';
|
||||||
|
|
||||||
const TEXT_ENCODER = new TextEncoder();
|
const TEXT_ENCODER = new TextEncoder();
|
||||||
@@ -64,9 +64,8 @@ export async function sealString(
|
|||||||
plaintext: string,
|
plaintext: string,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const key = km.fieldKey(table, column);
|
const key = km.fieldKey(table, column);
|
||||||
const nonce = deriveNonce(key, table, pk);
|
|
||||||
const aad = buildAad(table, column, pk);
|
const aad = buildAad(table, column, pk);
|
||||||
return aeadSeal(key, nonce, TEXT_ENCODER.encode(plaintext), aad);
|
return aeadSeal(key, randomNonce(), TEXT_ENCODER.encode(plaintext), aad);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decrypt a blob into a string, reconstructing AAD from (table, column, pk). */
|
/** Decrypt a blob into a string, reconstructing AAD from (table, column, pk). */
|
||||||
@@ -78,9 +77,8 @@ export async function openString(
|
|||||||
blob: Uint8Array,
|
blob: Uint8Array,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const key = km.fieldKey(table, column);
|
const key = km.fieldKey(table, column);
|
||||||
const expectedNonce = deriveNonce(key, table, pk);
|
|
||||||
const aad = buildAad(table, column, pk);
|
const aad = buildAad(table, column, pk);
|
||||||
const pt = await aeadOpen(key, blob, aad, expectedNonce);
|
const pt = await aeadOpen(key, blob, aad);
|
||||||
return TEXT_DECODER.decode(pt);
|
return TEXT_DECODER.decode(pt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +91,8 @@ export async function sealBytes(
|
|||||||
plaintext: Uint8Array,
|
plaintext: Uint8Array,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const key = km.fieldKey(table, column);
|
const key = km.fieldKey(table, column);
|
||||||
const nonce = deriveNonce(key, table, pk);
|
|
||||||
const aad = buildAad(table, column, pk);
|
const aad = buildAad(table, column, pk);
|
||||||
return aeadSeal(key, nonce, plaintext, aad);
|
return aeadSeal(key, randomNonce(), plaintext, aad);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Decrypt arbitrary bytes payload. */
|
/** Decrypt arbitrary bytes payload. */
|
||||||
@@ -107,9 +104,8 @@ export async function openBytes(
|
|||||||
blob: Uint8Array,
|
blob: Uint8Array,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const key = km.fieldKey(table, column);
|
const key = km.fieldKey(table, column);
|
||||||
const expectedNonce = deriveNonce(key, table, pk);
|
|
||||||
const aad = buildAad(table, column, pk);
|
const aad = buildAad(table, column, pk);
|
||||||
return aeadOpen(key, blob, aad, expectedNonce);
|
return aeadOpen(key, blob, aad);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Typed encoders for each StorageProvider entity ──────────────────────
|
// ─── Typed encoders for each StorageProvider entity ──────────────────────
|
||||||
|
|||||||
@@ -19,12 +19,16 @@ export {
|
|||||||
deriveBlobSlotId,
|
deriveBlobSlotId,
|
||||||
deriveBlobKey,
|
deriveBlobKey,
|
||||||
deriveBlobSigningSeed,
|
deriveBlobSigningSeed,
|
||||||
|
deriveVaultId,
|
||||||
|
deriveVaultContentKey,
|
||||||
|
deriveVaultSigningSeed,
|
||||||
} from './crypto/kdf.js';
|
} from './crypto/kdf.js';
|
||||||
export {
|
export {
|
||||||
AEAD_NONCE_LEN,
|
AEAD_NONCE_LEN,
|
||||||
AEAD_TAG_LEN,
|
AEAD_TAG_LEN,
|
||||||
aeadSeal,
|
aeadSeal,
|
||||||
aeadOpen,
|
aeadOpen,
|
||||||
|
randomNonce,
|
||||||
} from './crypto/aead.js';
|
} from './crypto/aead.js';
|
||||||
export { EncryptedSQLiteStorage } from './storage/encrypted-sqlite.js';
|
export { EncryptedSQLiteStorage } from './storage/encrypted-sqlite.js';
|
||||||
export {
|
export {
|
||||||
|
|||||||
141
packages/shade-storage-encrypted/tests/nonce-reuse.test.ts
Normal file
141
packages/shade-storage-encrypted/tests/nonce-reuse.test.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Regression tests for the AES-GCM nonce-reuse fix (G0).
|
||||||
|
*
|
||||||
|
* The codec used to derive its nonce from (fieldKey, table, pk) alone. That
|
||||||
|
* is a pure function of row identity, so every re-seal of a mutable row —
|
||||||
|
* `saveSession` runs on each ratchet step — reused (key, nonce) across
|
||||||
|
* different plaintexts. AES-GCM forbids exactly that: it leaks the XOR of
|
||||||
|
* the plaintexts and the GHASH subkey, which yields tag forgery under that
|
||||||
|
* key. These tests fail if the derivation ever comes back.
|
||||||
|
*/
|
||||||
|
import { describe, test, expect } from 'bun:test';
|
||||||
|
import { KeyManager } from '../src/crypto/key-manager.js';
|
||||||
|
import { AEAD_NONCE_LEN, aeadSeal } from '../src/crypto/aead.js';
|
||||||
|
import { buildAad, deriveNonce } from '../src/crypto/kdf.js';
|
||||||
|
import {
|
||||||
|
COL,
|
||||||
|
TBL,
|
||||||
|
openBytes,
|
||||||
|
openString,
|
||||||
|
sealBytes,
|
||||||
|
sealString,
|
||||||
|
} from '../src/crypto/row-codec.js';
|
||||||
|
|
||||||
|
const TEXT = new TextEncoder();
|
||||||
|
|
||||||
|
function km(): Promise<KeyManager> {
|
||||||
|
return KeyManager.open({ kind: 'injected', key: new Uint8Array(32).fill(0x42) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceOf = (blob: Uint8Array) => blob.subarray(0, AEAD_NONCE_LEN);
|
||||||
|
const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
describe('nonce uniqueness across re-saves', () => {
|
||||||
|
test('two seals of the same row do not share a nonce', async () => {
|
||||||
|
const k = await km();
|
||||||
|
// The real shape of the bug: same (table, column, pk), different
|
||||||
|
// plaintext, as a session row is re-sealed on every ratchet step.
|
||||||
|
const first = await sealString(k, TBL.sessions, COL.session, 'alice', '{"messageCount":1}');
|
||||||
|
const second = await sealString(k, TBL.sessions, COL.session, 'alice', '{"messageCount":2}');
|
||||||
|
expect(hex(nonceOf(first))).not.toBe(hex(nonceOf(second)));
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a long run of re-saves produces all-distinct nonces', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
const blob = await sealString(k, TBL.sessions, COL.session, 'alice', `state-${i}`);
|
||||||
|
seen.add(hex(nonceOf(blob)));
|
||||||
|
}
|
||||||
|
expect(seen.size).toBe(200);
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sealBytes is covered by the same rule', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const a = await sealBytes(k, TBL.config, COL.config, 'cfg', TEXT.encode('one'));
|
||||||
|
const b = await sealBytes(k, TBL.config, COL.config, 'cfg', TEXT.encode('two'));
|
||||||
|
expect(hex(nonceOf(a))).not.toBe(hex(nonceOf(b)));
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the nonce is not the derived one', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const blob = await sealString(k, TBL.sessions, COL.session, 'alice', 'payload');
|
||||||
|
const derived = deriveNonce(k.fieldKey(TBL.sessions, COL.session), TBL.sessions, 'alice');
|
||||||
|
expect(hex(nonceOf(blob))).not.toBe(hex(derived));
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('backward compatibility with deterministically-sealed blobs', () => {
|
||||||
|
// `aeadOpen` has always read the nonce from the blob prefix, so data
|
||||||
|
// written before the fix opens unchanged and needs no migration. This
|
||||||
|
// test reproduces an old blob by sealing with the deprecated derivation.
|
||||||
|
test('a blob sealed with the old derived nonce still opens', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const key = k.fieldKey(TBL.sessions, COL.session);
|
||||||
|
const legacy = await aeadSeal(
|
||||||
|
key,
|
||||||
|
deriveNonce(key, TBL.sessions, 'alice'),
|
||||||
|
TEXT.encode('written before the fix'),
|
||||||
|
buildAad(TBL.sessions, COL.session, 'alice'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const opened = await openString(k, TBL.sessions, COL.session, 'alice', legacy);
|
||||||
|
expect(opened).toBe('written before the fix');
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('openBytes reads an old blob too', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const key = k.fieldKey(TBL.config, COL.config);
|
||||||
|
const payload = TEXT.encode('legacy bytes');
|
||||||
|
const legacy = await aeadSeal(
|
||||||
|
key,
|
||||||
|
deriveNonce(key, TBL.config, 'cfg'),
|
||||||
|
payload,
|
||||||
|
buildAad(TBL.config, COL.config, 'cfg'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await openBytes(k, TBL.config, COL.config, 'cfg', legacy)).toEqual(payload);
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new and old blobs are both readable under one key', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const key = k.fieldKey(TBL.sessions, COL.session);
|
||||||
|
const legacy = await aeadSeal(
|
||||||
|
key,
|
||||||
|
deriveNonce(key, TBL.sessions, 'bob'),
|
||||||
|
TEXT.encode('old'),
|
||||||
|
buildAad(TBL.sessions, COL.session, 'bob'),
|
||||||
|
);
|
||||||
|
const fresh = await sealString(k, TBL.sessions, COL.session, 'bob', 'new');
|
||||||
|
|
||||||
|
expect(await openString(k, TBL.sessions, COL.session, 'bob', legacy)).toBe('old');
|
||||||
|
expect(await openString(k, TBL.sessions, COL.session, 'bob', fresh)).toBe('new');
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tamper detection survives the switch', () => {
|
||||||
|
// The dropped `expectedNonce` check was the stated reason for the
|
||||||
|
// deterministic nonce. It detected nothing the AEAD tag misses.
|
||||||
|
test('a flipped nonce byte is still rejected', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const blob = await sealString(k, TBL.sessions, COL.session, 'alice', 'payload');
|
||||||
|
const tampered = new Uint8Array(blob);
|
||||||
|
tampered[0]! ^= 0x01;
|
||||||
|
await expect(openString(k, TBL.sessions, COL.session, 'alice', tampered)).rejects.toThrow();
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a blob moved to another row is still rejected (AAD binding)', async () => {
|
||||||
|
const k = await km();
|
||||||
|
const blob = await sealString(k, TBL.sessions, COL.session, 'alice', 'alice-secret');
|
||||||
|
await expect(openString(k, TBL.sessions, COL.session, 'bob', blob)).rejects.toThrow();
|
||||||
|
k.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user