feat(client-encrypt): create @bytelyst/client-encrypt — Web Crypto API encryption
- AES-256-GCM via SubtleCrypto (browsers + React Native with polyfill) - Wire-compatible EncryptedField with @bytelyst/field-encrypt (server) and BLFieldEncrypt (Swift/Kotlin native SDKs) - encryptField, decryptField, generateKey, keyFromHex, keyToHex - PBKDF2 key derivation (600k iterations per OWASP 2023) - isEncryptedField type guard, toHex/fromHex helpers - 22 Vitest tests, all passing - Add Web Crypto globals to root ESLint config
This commit is contained in:
parent
9bb322113a
commit
1bce981f43
@ -57,6 +57,11 @@ export default [
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly',
|
||||
crypto: 'readonly',
|
||||
CryptoKey: 'readonly',
|
||||
SubtleCrypto: 'readonly',
|
||||
AesGcmParams: 'readonly',
|
||||
Crypto: 'readonly',
|
||||
ArrayBufferView: 'readonly',
|
||||
Blob: 'readonly',
|
||||
File: 'readonly',
|
||||
FormData: 'readonly',
|
||||
|
||||
23
packages/client-encrypt/package.json
Normal file
23
packages/client-encrypt/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@bytelyst/client-encrypt",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
181
packages/client-encrypt/src/aes-gcm.test.ts
Normal file
181
packages/client-encrypt/src/aes-gcm.test.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
encryptField,
|
||||
decryptField,
|
||||
generateKey,
|
||||
keyFromHex,
|
||||
keyToHex,
|
||||
deriveKey,
|
||||
} from './aes-gcm.js';
|
||||
import { isEncryptedField } from './guards.js';
|
||||
import { toHex, fromHex } from './hex.js';
|
||||
|
||||
describe('encryptField / decryptField', () => {
|
||||
it('roundtrip', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('Hello, World!', key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, key);
|
||||
expect(decrypted).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('empty string', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('', key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, key);
|
||||
expect(decrypted).toBe('');
|
||||
});
|
||||
|
||||
it('unicode', async () => {
|
||||
const key = await generateKey();
|
||||
const text = 'こんにちは世界 🌍 مرحبا Ñoño';
|
||||
const encrypted = await encryptField(text, key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, key);
|
||||
expect(decrypted).toBe(text);
|
||||
});
|
||||
|
||||
it('large payload', async () => {
|
||||
const key = await generateKey();
|
||||
const text = 'A'.repeat(100_000);
|
||||
const encrypted = await encryptField(text, key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, key);
|
||||
expect(decrypted).toBe(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EncryptedField structure', () => {
|
||||
it('has correct sentinel fields', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('test', key, 'dek_test');
|
||||
expect(encrypted.__encrypted).toBe(true);
|
||||
expect(encrypted.v).toBe(1);
|
||||
expect(encrypted.alg).toBe('aes-256-gcm');
|
||||
expect(encrypted.dekId).toBe('dek_test');
|
||||
});
|
||||
|
||||
it('has correct hex lengths', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('test', key, 'dek_test');
|
||||
expect(encrypted.iv.length).toBe(24); // 12 bytes = 24 hex
|
||||
expect(encrypted.tag.length).toBe(32); // 16 bytes = 32 hex
|
||||
expect(encrypted.ct.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('unique IVs per encryption', async () => {
|
||||
const key = await generateKey();
|
||||
const a = await encryptField('same', key, 'dek_test');
|
||||
const b = await encryptField('same', key, 'dek_test');
|
||||
expect(a.iv).not.toBe(b.iv);
|
||||
expect(a.ct).not.toBe(b.ct);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AAD (Additional Authenticated Data)', () => {
|
||||
it('roundtrip with AAD', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('secret', key, 'dek_test', 'user:ctx');
|
||||
const decrypted = await decryptField(encrypted, key, 'user:ctx');
|
||||
expect(decrypted).toBe('secret');
|
||||
});
|
||||
|
||||
it('wrong AAD fails', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('secret', key, 'dek_test', 'correct');
|
||||
await expect(decryptField(encrypted, key, 'wrong')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('missing AAD fails', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('secret', key, 'dek_test', 'some-aad');
|
||||
await expect(decryptField(encrypted, key)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrong key', () => {
|
||||
it('decrypt with wrong key fails', async () => {
|
||||
const key = await generateKey();
|
||||
const wrongKey = await generateKey();
|
||||
const encrypted = await encryptField('secret', key, 'dek_test');
|
||||
await expect(decryptField(encrypted, wrongKey)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyFromHex / keyToHex', () => {
|
||||
it('roundtrip', async () => {
|
||||
const key = await generateKey();
|
||||
const hex = await keyToHex(key);
|
||||
expect(hex.length).toBe(64); // 32 bytes = 64 hex chars
|
||||
const restored = await keyFromHex(hex);
|
||||
const encrypted = await encryptField('test', key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, restored);
|
||||
expect(decrypted).toBe('test');
|
||||
});
|
||||
|
||||
it('rejects invalid length', async () => {
|
||||
await expect(keyFromHex('aabb')).rejects.toThrow('32-byte key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveKey', () => {
|
||||
it('derives consistent key from passphrase + salt', async () => {
|
||||
const salt = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(salt);
|
||||
const key1 = await deriveKey('my-passphrase', salt, 1000, true);
|
||||
const key2 = await deriveKey('my-passphrase', salt, 1000, true);
|
||||
const hex1 = await keyToHex(key1);
|
||||
const hex2 = await keyToHex(key2);
|
||||
expect(hex1).toBe(hex2);
|
||||
});
|
||||
|
||||
it('different passphrases produce different keys', async () => {
|
||||
const salt = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(salt);
|
||||
const key1 = await deriveKey('pass-1', salt, 1000, true);
|
||||
const key2 = await deriveKey('pass-2', salt, 1000, true);
|
||||
const hex1 = await keyToHex(key1);
|
||||
const hex2 = await keyToHex(key2);
|
||||
expect(hex1).not.toBe(hex2);
|
||||
});
|
||||
|
||||
it('derived key can encrypt/decrypt', async () => {
|
||||
const salt = new Uint8Array(16);
|
||||
globalThis.crypto.getRandomValues(salt);
|
||||
const key = await deriveKey('test', salt, 1000, true);
|
||||
const encrypted = await encryptField('hello', key, 'dek_test');
|
||||
const decrypted = await decryptField(encrypted, key);
|
||||
expect(decrypted).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEncryptedField', () => {
|
||||
it('true for valid EncryptedField', async () => {
|
||||
const key = await generateKey();
|
||||
const encrypted = await encryptField('test', key, 'dek_test');
|
||||
expect(isEncryptedField(encrypted)).toBe(true);
|
||||
});
|
||||
|
||||
it('false for plain string', () => {
|
||||
expect(isEncryptedField('just a string')).toBe(false);
|
||||
});
|
||||
|
||||
it('false for null', () => {
|
||||
expect(isEncryptedField(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('false for incomplete object', () => {
|
||||
expect(isEncryptedField({ __encrypted: true, v: 1 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hex utilities', () => {
|
||||
it('toHex / fromHex roundtrip', () => {
|
||||
const bytes = new Uint8Array([0x00, 0x0f, 0xff, 0xab, 0xcd]);
|
||||
const hex = toHex(bytes);
|
||||
expect(hex).toBe('000fffabcd');
|
||||
const restored = fromHex(hex);
|
||||
expect(restored).toEqual(bytes);
|
||||
});
|
||||
|
||||
it('fromHex rejects odd length', () => {
|
||||
expect(() => fromHex('a')).toThrow('even length');
|
||||
});
|
||||
});
|
||||
215
packages/client-encrypt/src/aes-gcm.ts
Normal file
215
packages/client-encrypt/src/aes-gcm.ts
Normal file
@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @bytelyst/client-encrypt — AES-256-GCM via Web Crypto API
|
||||
*
|
||||
* Works in browsers (window.crypto.subtle) and React Native (expo-crypto polyfill).
|
||||
* Produces EncryptedField objects wire-compatible with:
|
||||
* - @bytelyst/field-encrypt (Node.js server)
|
||||
* - BLFieldEncrypt (Swift CryptoKit / Kotlin javax.crypto)
|
||||
*/
|
||||
|
||||
import type { EncryptedField } from './types.js';
|
||||
import { toHex, fromHex } from './hex.js';
|
||||
|
||||
const ALGORITHM = 'AES-GCM';
|
||||
const KEY_SIZE_BITS = 256;
|
||||
const IV_BYTES = 12;
|
||||
const TAG_BITS = 128;
|
||||
|
||||
/** Get the SubtleCrypto instance (browser or globalThis). */
|
||||
function getSubtle(): SubtleCrypto {
|
||||
if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
|
||||
return globalThis.crypto.subtle;
|
||||
}
|
||||
throw new Error(
|
||||
'@bytelyst/client-encrypt requires Web Crypto API (SubtleCrypto). ' +
|
||||
'Use a polyfill in React Native (e.g., expo-crypto).'
|
||||
);
|
||||
}
|
||||
|
||||
/** Get the crypto object for random bytes. */
|
||||
function getCrypto(): Crypto {
|
||||
if (typeof globalThis !== 'undefined' && globalThis.crypto) {
|
||||
return globalThis.crypto;
|
||||
}
|
||||
throw new Error('@bytelyst/client-encrypt requires globalThis.crypto for random bytes.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a plaintext string with AES-256-GCM using Web Crypto API.
|
||||
*
|
||||
* @param plaintext - UTF-8 string to encrypt
|
||||
* @param key - CryptoKey (AES-GCM, 256-bit)
|
||||
* @param dekId - DEK identifier stored in the output
|
||||
* @param aad - Optional additional authenticated data
|
||||
* @returns EncryptedField with hex-encoded ciphertext, IV, and tag
|
||||
*/
|
||||
export async function encryptField(
|
||||
plaintext: string,
|
||||
key: CryptoKey,
|
||||
dekId: string,
|
||||
aad?: string
|
||||
): Promise<EncryptedField> {
|
||||
const subtle = getSubtle();
|
||||
const crypto = getCrypto();
|
||||
|
||||
const iv = new Uint8Array(IV_BYTES);
|
||||
crypto.getRandomValues(iv);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const plaintextBytes = encoder.encode(plaintext);
|
||||
|
||||
const params: AesGcmParams = {
|
||||
name: ALGORITHM,
|
||||
iv: iv.buffer as ArrayBuffer,
|
||||
tagLength: TAG_BITS,
|
||||
};
|
||||
|
||||
if (aad) {
|
||||
params.additionalData = encoder.encode(aad).buffer as ArrayBuffer;
|
||||
}
|
||||
|
||||
// Web Crypto returns ciphertext || tag concatenated
|
||||
const ciphertextWithTag = new Uint8Array(
|
||||
await subtle.encrypt(params, key, plaintextBytes.buffer as ArrayBuffer)
|
||||
);
|
||||
|
||||
const tagOffset = ciphertextWithTag.length - TAG_BITS / 8;
|
||||
const ct = ciphertextWithTag.slice(0, tagOffset);
|
||||
const tag = ciphertextWithTag.slice(tagOffset);
|
||||
|
||||
return {
|
||||
__encrypted: true,
|
||||
v: 1,
|
||||
alg: 'aes-256-gcm',
|
||||
ct: toHex(ct),
|
||||
iv: toHex(iv),
|
||||
tag: toHex(tag),
|
||||
dekId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an EncryptedField back to plaintext.
|
||||
*
|
||||
* @param field - EncryptedField object
|
||||
* @param key - CryptoKey (must match the key used to encrypt)
|
||||
* @param aad - Optional AAD (must match the AAD used during encryption)
|
||||
* @returns Decrypted UTF-8 string
|
||||
* @throws DOMException if authentication tag verification fails
|
||||
*/
|
||||
export async function decryptField(
|
||||
field: EncryptedField,
|
||||
key: CryptoKey,
|
||||
aad?: string
|
||||
): Promise<string> {
|
||||
const subtle = getSubtle();
|
||||
|
||||
const iv = fromHex(field.iv);
|
||||
const ct = fromHex(field.ct);
|
||||
const tag = fromHex(field.tag);
|
||||
|
||||
// Web Crypto expects ciphertext || tag concatenated
|
||||
const ciphertextWithTag = new Uint8Array(ct.length + tag.length);
|
||||
ciphertextWithTag.set(ct, 0);
|
||||
ciphertextWithTag.set(tag, ct.length);
|
||||
|
||||
const params: AesGcmParams = {
|
||||
name: ALGORITHM,
|
||||
iv: iv.buffer as ArrayBuffer,
|
||||
tagLength: TAG_BITS,
|
||||
};
|
||||
|
||||
if (aad) {
|
||||
params.additionalData = new TextEncoder().encode(aad).buffer as ArrayBuffer;
|
||||
}
|
||||
|
||||
const plaintextBytes = new Uint8Array(
|
||||
await subtle.decrypt(params, key, ciphertextWithTag.buffer as ArrayBuffer)
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(plaintextBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random AES-256-GCM CryptoKey.
|
||||
*
|
||||
* @param extractable - Whether the key material can be exported (default: true).
|
||||
* Set to `false` for non-extractable keys stored in IndexedDB.
|
||||
*/
|
||||
export async function generateKey(extractable = true): Promise<CryptoKey> {
|
||||
const subtle = getSubtle();
|
||||
return subtle.generateKey({ name: ALGORITHM, length: KEY_SIZE_BITS }, extractable, [
|
||||
'encrypt',
|
||||
'decrypt',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a hex-encoded key string as a CryptoKey.
|
||||
*
|
||||
* @param hex - 64 hex chars = 32 bytes
|
||||
* @param extractable - Whether the imported key can be exported (default: true)
|
||||
*/
|
||||
export async function keyFromHex(hex: string, extractable = true): Promise<CryptoKey> {
|
||||
const subtle = getSubtle();
|
||||
const keyBytes = fromHex(hex);
|
||||
if (keyBytes.length !== KEY_SIZE_BITS / 8) {
|
||||
throw new Error(`AES-256-GCM requires a 32-byte key, got ${keyBytes.length}`);
|
||||
}
|
||||
return subtle.importKey(
|
||||
'raw',
|
||||
keyBytes.buffer as ArrayBuffer,
|
||||
{ name: ALGORITHM, length: KEY_SIZE_BITS },
|
||||
extractable,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a CryptoKey to a hex-encoded string.
|
||||
* Only works if the key was created with `extractable: true`.
|
||||
*/
|
||||
export async function keyToHex(key: CryptoKey): Promise<string> {
|
||||
const subtle = getSubtle();
|
||||
const raw = new Uint8Array(await subtle.exportKey('raw', key));
|
||||
return toHex(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an AES-256 key from a passphrase using PBKDF2.
|
||||
*
|
||||
* @param passphrase - User passphrase
|
||||
* @param salt - Random salt (at least 16 bytes recommended)
|
||||
* @param iterations - PBKDF2 iterations (default: 600,000 per OWASP 2023)
|
||||
* @param extractable - Whether derived key can be exported (default: false)
|
||||
*/
|
||||
export async function deriveKey(
|
||||
passphrase: string,
|
||||
salt: Uint8Array,
|
||||
iterations = 600_000,
|
||||
extractable = false
|
||||
): Promise<CryptoKey> {
|
||||
const subtle = getSubtle();
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const baseKey = await subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(passphrase).buffer as ArrayBuffer,
|
||||
'PBKDF2',
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
|
||||
return subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt.buffer as ArrayBuffer,
|
||||
iterations,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
baseKey,
|
||||
{ name: ALGORITHM, length: KEY_SIZE_BITS },
|
||||
extractable,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
22
packages/client-encrypt/src/guards.ts
Normal file
22
packages/client-encrypt/src/guards.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @bytelyst/client-encrypt — Type guards
|
||||
*
|
||||
* Compatible with @bytelyst/field-encrypt isEncryptedField() on the server.
|
||||
*/
|
||||
|
||||
import type { EncryptedField } from './types.js';
|
||||
|
||||
/** Check if a value is an EncryptedField object. */
|
||||
export function isEncryptedField(value: unknown): value is EncryptedField {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
const obj = value as Record<string, unknown>;
|
||||
return (
|
||||
obj.__encrypted === true &&
|
||||
obj.v !== undefined &&
|
||||
obj.alg !== undefined &&
|
||||
typeof obj.ct === 'string' &&
|
||||
typeof obj.iv === 'string' &&
|
||||
typeof obj.tag === 'string' &&
|
||||
typeof obj.dekId === 'string'
|
||||
);
|
||||
}
|
||||
28
packages/client-encrypt/src/hex.ts
Normal file
28
packages/client-encrypt/src/hex.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @bytelyst/client-encrypt — Hex encoding utilities
|
||||
*
|
||||
* Converts between Uint8Array and hex strings.
|
||||
* Compatible with the hex encoding used by @bytelyst/field-encrypt (Node.js)
|
||||
* and BLFieldEncrypt (Swift/Kotlin).
|
||||
*/
|
||||
|
||||
/** Encode a Uint8Array to a lowercase hex string. */
|
||||
export function toHex(bytes: Uint8Array): string {
|
||||
const parts: string[] = new Array(bytes.length);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
parts[i] = bytes[i].toString(16).padStart(2, '0');
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/** Decode a hex string to a Uint8Array. */
|
||||
export function fromHex(hex: string): Uint8Array {
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error(`Hex string must have even length, got ${hex.length}`);
|
||||
}
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
36
packages/client-encrypt/src/index.ts
Normal file
36
packages/client-encrypt/src/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @bytelyst/client-encrypt
|
||||
*
|
||||
* Client-side AES-256-GCM field encryption using Web Crypto API.
|
||||
* Works in browsers and React Native (with SubtleCrypto polyfill).
|
||||
* Wire-compatible with @bytelyst/field-encrypt (server) and
|
||||
* BLFieldEncrypt (Swift/Kotlin native SDKs).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { generateKey, encryptField, decryptField } from '@bytelyst/client-encrypt';
|
||||
*
|
||||
* const key = await generateKey();
|
||||
* const encrypted = await encryptField('sensitive data', key, 'dek_user1_notes');
|
||||
* const plaintext = await decryptField(encrypted, key);
|
||||
* ```
|
||||
*/
|
||||
|
||||
// ── Main API ────────────────────────────────────────
|
||||
export {
|
||||
encryptField,
|
||||
decryptField,
|
||||
generateKey,
|
||||
keyFromHex,
|
||||
keyToHex,
|
||||
deriveKey,
|
||||
} from './aes-gcm.js';
|
||||
|
||||
// ── Type guards ─────────────────────────────────────
|
||||
export { isEncryptedField } from './guards.js';
|
||||
|
||||
// ── Hex utilities ───────────────────────────────────
|
||||
export { toHex, fromHex } from './hex.js';
|
||||
|
||||
// ── Types ───────────────────────────────────────────
|
||||
export type { EncryptedField, ClientEncryptContext } from './types.js';
|
||||
33
packages/client-encrypt/src/types.ts
Normal file
33
packages/client-encrypt/src/types.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @bytelyst/client-encrypt — Types
|
||||
*
|
||||
* Shared type definitions for client-side field encryption.
|
||||
* Wire-compatible with @bytelyst/field-encrypt (server) and
|
||||
* BLFieldEncrypt (Swift/Kotlin native SDKs).
|
||||
*/
|
||||
|
||||
/** Encrypted field stored in Cosmos DB or API responses. */
|
||||
export interface EncryptedField {
|
||||
/** Sentinel — always true for encrypted fields. */
|
||||
readonly __encrypted: true;
|
||||
/** Schema version for future algorithm changes. */
|
||||
readonly v: 1;
|
||||
/** Algorithm identifier. */
|
||||
readonly alg: 'aes-256-gcm';
|
||||
/** Ciphertext (hex-encoded). */
|
||||
readonly ct: string;
|
||||
/** Initialization vector (hex-encoded, 12 bytes / 24 hex chars). */
|
||||
readonly iv: string;
|
||||
/** GCM authentication tag (hex-encoded, 16 bytes / 32 hex chars). */
|
||||
readonly tag: string;
|
||||
/** DEK identifier — identifies which key to use for decryption. */
|
||||
readonly dekId: string;
|
||||
}
|
||||
|
||||
/** Options for encrypt/decrypt operations. */
|
||||
export interface ClientEncryptContext {
|
||||
/** Scope for DEK isolation (typically userId). */
|
||||
readonly userId: string;
|
||||
/** Additional context for DEK naming and AAD (e.g., 'transcripts', 'notes'). */
|
||||
readonly context: string;
|
||||
}
|
||||
10
packages/client-encrypt/tsconfig.json
Normal file
10
packages/client-encrypt/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user