snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import { generate, decrypt, encrypt, exportJwk, importKey, type Key } from './aes';
describe('aes', () => {
describe('256-gcm', () => {
let key: Key<{ name: 'AES-GCM'; length: 256 }>;
beforeAll(async () => {
key = await generate();
});
it('generate a key with correct properties', async () => {
expect<256>(key.algorithm.length).toBe(256);
expect<'AES-GCM'>(key.algorithm.name).toBe('AES-GCM');
expect<true>(key.extractable).toBe(true);
expect<'secret'>(key.type).toBe('secret');
expect<2>(key.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(key.usages).toEqual(['encrypt', 'decrypt']);
});
it('should encrypt and decrypt data correctly', async () => {
const plaintext = new TextEncoder().encode('Hello, world!');
const ciphertext = await encrypt(key, plaintext);
const decrypted = await decrypt(key, ciphertext);
expect(decrypted).toBe('Hello, world!');
});
it('should export and re-import the key correctly', async () => {
const jwk = await exportJwk(key);
const importedKey = await importKey(jwk);
expect<256>(importedKey.algorithm.length).toBe(256);
expect<'AES-GCM'>(importedKey.algorithm.name).toBe('AES-GCM');
expect<true>(importedKey.extractable).toBe(true);
expect<'secret'>(importedKey.type).toBe('secret');
expect<2>(importedKey.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(importedKey.usages).toEqual(['encrypt', 'decrypt']);
});
});
describe('128-cbc', () => {
let key: Key<{ name: 'AES-CBC'; length: 128 }>;
beforeAll(async () => {
key = await importKey({ alg: 'A128CBC', ext: true, k: 'qb8In0Rpa9nwSusvxxDcbQ', key_ops: ['encrypt', 'decrypt'], kty: 'oct' });
});
it('import a key with correct properties', async () => {
expect<128>(key.algorithm.length).toBe(128);
expect<'AES-CBC'>(key.algorithm.name).toBe('AES-CBC');
expect<true>(key.extractable).toBe(true);
expect<'secret'>(key.type).toBe('secret');
expect<2>(key.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(key.usages).toEqual(['encrypt', 'decrypt']);
});
it('should encrypt and decrypt data correctly', async () => {
const plaintext = new TextEncoder().encode('Hello, AES-CBC!');
const ciphertext = await encrypt(key, plaintext);
const decrypted = await decrypt(key, ciphertext);
expect(decrypted).toBe('Hello, AES-CBC!');
});
it('should export and re-import the key correctly', async () => {
const jwk = await exportJwk(key);
const importedKey = await importKey(jwk);
expect<128>(importedKey.algorithm.length).toBe(128);
expect<'AES-CBC'>(importedKey.algorithm.name).toBe('AES-CBC');
expect<true>(importedKey.extractable).toBe(true);
expect<'secret'>(importedKey.type).toBe('secret');
expect<2>(importedKey.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(importedKey.usages).toEqual(['encrypt', 'decrypt']);
});
});
it('should throw on unsupported JWK alg', async () => {
await expect(
importKey({
// @ts-expect-error testing invalid alg
alg: 'A128GCM',
ext: true,
k: 'qb8In0Rpa9nwSusvxxDcbQ',
key_ops: ['encrypt', 'decrypt'],
kty: 'oct',
}),
).rejects.toThrow('Unrecognized algorithm name');
await expect(
importKey({
// @ts-expect-error testing invalid alg
alg: 'A256CBC',
ext: true,
k: '9o1xoHt4OamRJvnaLna-5akUb5L98S_iWYGGaXPZ1Yg',
key_ops: ['encrypt', 'decrypt'],
kty: 'oct',
}),
).rejects.toThrow('Unrecognized algorithm name');
});
});

View File

@@ -0,0 +1,62 @@
import { importJwk, exportKey, getRandomValues, generateKey, type IKey, type Exported, encryptBuffer, decryptBuffer } from './shared';
type AlgorithmMap = {
A256GCM: { name: 'AES-GCM'; length: 256 };
A128CBC: { name: 'AES-CBC'; length: 128 };
};
const ALGORITHM_MAP: AlgorithmMap = {
A256GCM: { name: 'AES-GCM', length: 256 },
A128CBC: { name: 'AES-CBC', length: 128 },
};
type Jwa = keyof AlgorithmMap;
type Algorithms = AlgorithmMap[Jwa];
export type Key<TAlgorithm extends Algorithms = Algorithms, TExtractable extends CryptoKey['extractable'] = true> = IKey<
TAlgorithm,
TExtractable,
'secret',
['encrypt', 'decrypt']
>;
export type Jwk<TJwa extends Jwa = Jwa> = {
kty: 'oct';
k: string;
key_ops: ['encrypt', 'decrypt'] | ['decrypt', 'encrypt'];
ext: true;
alg: TJwa;
};
type AesEncryptedContent = {
iv: Uint8Array<ArrayBuffer>;
ciphertext: Uint8Array<ArrayBuffer>;
};
export const importKey = <const TJwa extends Jwa>(jwk: Jwk<TJwa>): Promise<Key<AlgorithmMap[(typeof jwk)['alg']]>> => {
return importJwk(jwk, ALGORITHM_MAP[jwk.alg], true, ['encrypt', 'decrypt']);
};
export const exportJwk = <TAlgorithm extends Algorithms>(key: Key<TAlgorithm>): Promise<Exported<'jwk', Key<TAlgorithm>>> => {
return exportKey('jwk', key);
};
export const generate = (): Promise<Key<{ name: 'AES-GCM'; length: 256 }>> => {
return generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
};
export const decrypt = async (key: Key, content: AesEncryptedContent): Promise<string> => {
const decrypted = await decryptBuffer(
key,
{ name: key.algorithm.name, iv: content.iv } satisfies AesGcmParams | AesCbcParams,
content.ciphertext,
);
return new TextDecoder().decode(decrypted);
};
export const encrypt = async (key: Key, plaintext: Uint8Array<ArrayBuffer>): Promise<AesEncryptedContent> => {
const ivLength = key.algorithm.name === 'AES-GCM' ? 12 : 16;
const iv = getRandomValues(new Uint8Array(ivLength));
const ciphertext = await encryptBuffer(key, { name: key.algorithm.name, iv }, plaintext);
return { iv, ciphertext: new Uint8Array(ciphertext) };
};

View File

@@ -0,0 +1,49 @@
import { importBaseKey, derive, importKey } from './pbkdf2';
describe('pbkdf2', () => {
it('should import a base key', async () => {
const keyData = new Uint8Array([1, 2, 3, 4, 5]);
const baseKey = await importBaseKey(keyData);
expect<'PBKDF2'>(baseKey.algorithm.name).toBe('PBKDF2');
expect<false>(baseKey.extractable).toBe(false);
expect<'secret'>(baseKey.type).toBe('secret');
expect<1>(baseKey.usages.length).toBe(1);
expect<'deriveBits'>(baseKey.usages[0]).toBe('deriveBits');
});
it('should derive bits from a base key', async () => {
const keyData = new Uint8Array([1, 2, 3, 4, 5]);
const salt = new Uint8Array([5, 4, 3, 2, 1]);
const iterations = 1000;
const baseKey = await importBaseKey(keyData);
const derivedBits = await derive(baseKey, { salt, iterations });
expect<false>(derivedBits.detached).toBe(false);
expect<false>(derivedBits.resizable).toBe(false);
expect<32>(derivedBits.byteLength).toBe(32);
expect<32>(derivedBits.maxByteLength).toBe(32);
expect<() => never>(() => derivedBits.resize(32)).toThrow();
});
it('should import a derived key for AES-CBC', async () => {
const baseKey = await importBaseKey(new Uint8Array([1, 2, 3, 4, 5]));
const derivedBits = await derive(baseKey, { salt: new Uint8Array([5, 4, 3, 2, 1]), iterations: 1000 });
const derivedKey = await importKey(derivedBits, { name: 'AES-CBC', length: 256 });
expect<256>(derivedKey.algorithm.length).toBe(256);
expect<'AES-CBC'>(derivedKey.algorithm.name).toBe('AES-CBC');
expect<false>(derivedKey.extractable).toBe(false);
expect<'secret'>(derivedKey.type).toBe('secret');
expect<1>(derivedKey.usages.length).toBe(1);
expect<['decrypt']>(derivedKey.usages).toEqual(['decrypt']);
});
it('should import a derived key for AES-GCM', async () => {
const baseKey = await importBaseKey(new Uint8Array([1, 2, 3, 4, 5]));
const derivedBits = await derive(baseKey, { salt: new Uint8Array([5, 4, 3, 2, 1]), iterations: 1000 });
const derivedKey = await importKey(derivedBits, { name: 'AES-GCM', length: 256 });
expect<256>(derivedKey.algorithm.length).toBe(256);
expect<'AES-GCM'>(derivedKey.algorithm.name).toBe('AES-GCM');
expect<false>(derivedKey.extractable).toBe(false);
expect<'secret'>(derivedKey.type).toBe('secret');
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(derivedKey.usages).toEqual(['encrypt', 'decrypt']);
});
});

View File

@@ -0,0 +1,87 @@
import { importRaw, getRandomValues, decryptBuffer, encryptBuffer, deriveBits, type IKey } from './shared';
type Algorithms = { name: 'AES-GCM'; length: 256 } | { name: 'AES-CBC'; length: 256 };
export type DerivedKey<TAlgorithm extends Algorithms = Algorithms> = IKey<
TAlgorithm,
false,
'secret',
TAlgorithm['name'] extends 'AES-CBC' ? ['decrypt'] : ['encrypt', 'decrypt']
>;
export type Options = {
salt: Uint8Array<ArrayBuffer>;
iterations: number;
};
export type EncryptedContent = {
iv: Uint8Array<ArrayBuffer>;
ciphertext: Uint8Array<ArrayBuffer>;
};
type Narrow<T, U extends { [P in keyof T]?: T[P] }> = {
[P in keyof T]: P extends keyof U ? U[P] : T[P];
};
export type BaseKey = IKey<
{
readonly name: 'PBKDF2';
},
false,
'secret',
['deriveBits']
>;
export const importBaseKey = async (keyData: Uint8Array<ArrayBuffer>): Promise<BaseKey> => {
const baseKey = await importRaw(keyData, { name: 'PBKDF2' }, false, ['deriveBits']);
return baseKey;
};
type Throws<F> = F extends (...args: infer TArgs) => infer TRet ? (...args: TArgs) => TRet & never : never;
type FixedSizeArrayBuffer<N extends number> = Narrow<
ArrayBuffer,
{
resize: Throws<ArrayBuffer['resize']>;
readonly byteLength: N;
get maxByteLength(): N;
get resizable(): false;
get detached(): false;
}
>;
export type DerivedBits = FixedSizeArrayBuffer<32>;
export const derive = async (key: BaseKey, options: Options): Promise<DerivedBits> => {
const bits = await deriveBits(
{ name: key.algorithm.name, hash: 'SHA-256', salt: options.salt, iterations: options.iterations },
key,
256,
);
return bits as DerivedBits;
};
export const importKey = async <T extends Algorithms>(derivedBits: DerivedBits, algorithm: T): Promise<DerivedKey<T>> => {
const usages: ['decrypt'] | ['encrypt', 'decrypt'] = algorithm.name === 'AES-CBC' ? ['decrypt'] : ['encrypt', 'decrypt'];
const key = await importRaw(derivedBits, algorithm satisfies AesKeyGenParams, false, usages);
return key as DerivedKey<T>;
};
export const decrypt = async (key: DerivedKey, content: EncryptedContent): Promise<Uint8Array<ArrayBuffer>> => {
const decrypted = await decryptBuffer(
key,
{ name: key.algorithm.name, iv: content.iv } satisfies AesCbcParams | AesGcmParams,
content.ciphertext,
);
return new Uint8Array(decrypted);
};
export const encrypt = async (
key: DerivedKey<{ name: 'AES-GCM'; length: 256 }>,
data: Uint8Array<ArrayBuffer>,
): Promise<EncryptedContent> => {
// Always use AES-GCM for new data
const iv = getRandomValues(new Uint8Array(12));
const ciphertext = await encryptBuffer(key, { name: 'AES-GCM', iv }, data);
return { iv, ciphertext: new Uint8Array(ciphertext) };
};

View File

@@ -0,0 +1,65 @@
import { generate, decrypt, encrypt, exportPublicKey, exportPrivateKey, importPrivateKey, importPublicKey, type KeyPair } from './rsa';
describe('rsa', () => {
let keyPair: KeyPair;
beforeAll(async () => {
keyPair = await generate();
});
it('generate a key pair with correct properties', async () => {
expect<'RSA-OAEP'>(keyPair.publicKey.algorithm.name).toBe('RSA-OAEP');
expect<2048>(keyPair.publicKey.algorithm.modulusLength).toBe(2048);
expect<true>(keyPair.publicKey.extractable).toBe(true);
expect<'public'>(keyPair.publicKey.type).toBe('public');
expect<1>(keyPair.publicKey.usages.length).toBe(1);
expect<['encrypt']>(keyPair.publicKey.usages).toEqual(['encrypt']);
expect<'RSA-OAEP'>(keyPair.privateKey.algorithm.name).toBe('RSA-OAEP');
expect<2048>(keyPair.privateKey.algorithm.modulusLength).toBe(2048);
expect<true>(keyPair.privateKey.extractable).toBe(true);
expect<'private'>(keyPair.privateKey.type).toBe('private');
expect<1>(keyPair.privateKey.usages.length).toBe(1);
expect<['decrypt']>(keyPair.privateKey.usages).toEqual(['decrypt']);
});
it('should encrypt and decrypt data correctly', async () => {
const plaintext = new TextEncoder().encode('Hello, RSA-OAEP!');
const ciphertext = await encrypt(keyPair.publicKey, plaintext);
const decrypted = await decrypt(keyPair.privateKey, ciphertext);
expect(new TextDecoder().decode(decrypted)).toBe('Hello, RSA-OAEP!');
});
it('should export and re-import the keys correctly', async () => {
const publicJwk = await exportPublicKey(keyPair.publicKey);
const privateJwk = await exportPrivateKey(keyPair.privateKey);
expect<'RSA-OAEP-256'>(publicJwk.alg).toBe('RSA-OAEP-256');
expect<'RSA'>(publicJwk.kty).toBe('RSA');
expect<true>(publicJwk.ext).toBe(true);
expect<'AQAB'>(publicJwk.e).toBe('AQAB');
expect<'RSA-OAEP-256'>(privateJwk.alg).toBe('RSA-OAEP-256');
expect<'RSA'>(privateJwk.kty).toBe('RSA');
expect<true>(privateJwk.ext).toBe(true);
expect<'AQAB'>(privateJwk.e).toBe('AQAB');
expect(privateJwk.n).toEqual(publicJwk.n);
const importedPublicKey = await importPublicKey(publicJwk);
expect<2048>(importedPublicKey.algorithm.modulusLength).toBe(2048);
expect<'RSA-OAEP'>(importedPublicKey.algorithm.name).toBe('RSA-OAEP');
expect<true>(importedPublicKey.extractable).toBe(true);
expect<'public'>(importedPublicKey.type).toBe('public');
expect<1>(importedPublicKey.usages.length).toBe(1);
expect<['encrypt']>(importedPublicKey.usages).toEqual(['encrypt']);
const importedPrivateKey = await importPrivateKey(privateJwk);
expect<2048>(importedPrivateKey.algorithm.modulusLength).toBe(2048);
expect<'RSA-OAEP'>(importedPrivateKey.algorithm.name).toBe('RSA-OAEP');
expect<true>(importedPrivateKey.extractable).toBe(true);
expect<'private'>(importedPrivateKey.type).toBe('private');
expect<1>(importedPrivateKey.usages.length).toBe(1);
expect<['decrypt']>(importedPrivateKey.usages).toEqual(['decrypt']);
});
});

View File

@@ -0,0 +1,109 @@
import { generateKeyPair, exportKey, importJwk, type IKeyPair, encryptBuffer, decryptBuffer } from './shared';
export type KeyPair = IKeyPair<
{
readonly name: 'RSA-OAEP';
readonly modulusLength: 2048;
readonly publicExponent: Uint8Array<ArrayBuffer>;
readonly hash: {
readonly name: 'SHA-256';
};
},
true,
['encrypt', 'decrypt']
>;
export type PublicKey = KeyPair['publicKey'];
export type PrivateKey = KeyPair['privateKey'];
export const generate = async (): Promise<KeyPair> => {
const keyPair = await generateKeyPair(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
},
true,
['encrypt', 'decrypt'],
);
return keyPair;
};
type Base64Url = string;
export interface IPublicJwk {
kty: 'RSA';
alg: 'RSA-OAEP-256';
e: 'AQAB';
ext: true;
key_ops: ['encrypt'];
n: Base64Url;
}
export interface IPrivateJwk {
kty: 'RSA';
alg: 'RSA-OAEP-256';
e: 'AQAB';
ext: true;
d: Base64Url;
dp: Base64Url;
dq: Base64Url;
key_ops: ['decrypt'];
n: Base64Url;
p: Base64Url;
q: Base64Url;
qi: Base64Url;
}
export const exportPublicKey = async (key: PublicKey): Promise<IPublicJwk> => {
const jwk = await exportKey('jwk', key);
return jwk as IPublicJwk;
};
export const exportPrivateKey = async (key: PrivateKey): Promise<IPrivateJwk> => {
const jwk = await exportKey('jwk', key);
return jwk as IPrivateJwk;
};
export const importPrivateKey = async (keyData: IPrivateJwk): Promise<PrivateKey> => {
const key = await importJwk(
keyData,
{
name: 'RSA-OAEP',
hash: { name: 'SHA-256' },
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
},
true,
['decrypt'],
);
return key as PrivateKey;
};
export const importPublicKey = async (keyData: IPublicJwk): Promise<PublicKey> => {
const key = await importJwk(
keyData,
{
name: 'RSA-OAEP',
hash: { name: 'SHA-256' },
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
},
true,
['encrypt'],
);
return key as PublicKey;
};
export const encrypt = async (key: PublicKey, data: BufferSource): Promise<Uint8Array<ArrayBuffer>> => {
const encrypted = await encryptBuffer(key, { name: key.algorithm.name }, data);
return new Uint8Array(encrypted);
};
export const decrypt = async (key: PrivateKey, data: BufferSource): Promise<Uint8Array<ArrayBuffer>> => {
const decrypted = await decryptBuffer(key, { name: key.algorithm.name }, data);
return new Uint8Array(decrypted);
};

View File

@@ -0,0 +1,141 @@
import { generateKey, generateKeyPair, exportKey, importJwk, encryptBuffer, decryptBuffer, getRandomValues } from './shared';
describe('Shared Crypto Functions', () => {
describe('generateKey', () => {
it('should generate an AES key with correct properties', async () => {
const key = await generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
expect<'AES-GCM'>(key.algorithm.name).toBe('AES-GCM');
expect<256>(key.algorithm.length).toBe(256);
expect<true>(key.extractable).toBe(true);
expect<'secret'>(key.type).toBe('secret');
expect<2>(key.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(key.usages).toEqual(expect.arrayContaining(['encrypt', 'decrypt']));
});
it('should export a key to JWK format', async () => {
const key = await generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt']);
const exportedKey = await exportKey('jwk', key);
expect(Object.keys(exportedKey)).toHaveLength(5);
expect<true>(exportedKey.ext).toBe(true);
expect<'oct'>(exportedKey.kty).toBe('oct');
expect<string>(exportedKey.k).toHaveLength(22); // 128 bits in base64url
expect<`A128CBC`>(exportedKey.alg).toBe('A128CBC');
expect<['encrypt']>(exportedKey.key_ops).toEqual(['encrypt']);
});
});
describe('generateKeyPair', () => {
it('should generate an RSA key pair with correct properties', async () => {
const keyPair = await generateKeyPair(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
},
true,
['encrypt', 'decrypt'],
);
expect<'RSA-OAEP'>(keyPair.publicKey.algorithm.name).toBe('RSA-OAEP');
expect<2048>(keyPair.publicKey.algorithm.modulusLength).toBe(2048);
expect<true>(keyPair.publicKey.extractable).toBe(true);
expect<'public'>(keyPair.publicKey.type).toBe('public');
expect<1>(keyPair.publicKey.usages.length).toBe(1);
expect<['encrypt']>(keyPair.publicKey.usages).toEqual(['encrypt']);
expect<'RSA-OAEP'>(keyPair.privateKey.algorithm.name).toBe('RSA-OAEP');
expect<2048>(keyPair.privateKey.algorithm.modulusLength).toBe(2048);
expect<true>(keyPair.privateKey.extractable).toBe(true);
expect<'private'>(keyPair.privateKey.type).toBe('private');
expect<1>(keyPair.privateKey.usages.length).toBe(1);
expect<['decrypt']>(keyPair.privateKey.usages).toEqual(['decrypt']);
});
});
describe('importKey', () => {
it('should import an AES key from JWK format', async () => {
const key = await importJwk(
{
kty: 'oct' as const,
k: 'qb8In0Rpa9nwSusvxxDcbQ',
key_ops: ['encrypt', 'decrypt'] as const,
ext: true,
alg: 'A128CBC' as const,
},
{ name: 'AES-CBC', length: 128 },
true,
['encrypt', 'decrypt'],
);
expect<128>(key.algorithm.length).toBe(128);
expect<'AES-CBC'>(key.algorithm.name).toBe('AES-CBC');
expect<true>(key.extractable).toBe(true);
expect<'secret'>(key.type).toBe('secret');
expect<2>(key.usages.length).toBe(2);
expect<['encrypt', 'decrypt'] | ['decrypt', 'encrypt']>(key.usages).toEqual(expect.arrayContaining(['encrypt', 'decrypt']));
});
});
describe('exportKey', () => {
it('should export an RSA public key to JWK format', async () => {
const keyPair = await generateKeyPair(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
},
true,
['encrypt', 'decrypt'],
);
const exportedKey = await exportKey('jwk', keyPair.publicKey);
expect(Object.keys(exportedKey).toSorted()).toEqual(['alg', 'e', 'ext', 'key_ops', 'kty', 'n'].toSorted());
expect<true>(exportedKey.ext).toBe(true);
expect<'RSA'>(exportedKey.kty).toBe('RSA');
expect<`RSA-OAEP-256`>(exportedKey.alg).toBe('RSA-OAEP-256');
expect<['encrypt']>(exportedKey.key_ops).toEqual(['encrypt']);
expect<string>(exportedKey.n).toBeDefined();
expect<string>(exportedKey.e).toBeDefined();
});
it('should export an RSA private key to JWK format', async () => {
const keyPair = await generateKeyPair(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
},
true,
['encrypt', 'decrypt'],
);
const exportedKey = await exportKey('jwk', keyPair.privateKey);
expect(Object.keys(exportedKey).toSorted()).toEqual(
['alg', 'd', 'dp', 'dq', 'e', 'ext', 'key_ops', 'kty', 'n', 'p', 'q', 'qi'].toSorted(),
);
expect<true>(exportedKey.ext).toBe(true);
expect<'RSA'>(exportedKey.kty).toBe('RSA');
expect<`RSA-OAEP-256`>(exportedKey.alg).toBe('RSA-OAEP-256');
expect<['decrypt']>(exportedKey.key_ops).toEqual(['decrypt']);
expect<string>(exportedKey.n).toBeDefined();
expect<string>(exportedKey.e).toBeDefined();
expect<string>(exportedKey.d).toBeDefined();
expect<string>(exportedKey.p).toBeDefined();
expect<string>(exportedKey.q).toBeDefined();
expect<string>(exportedKey.dp).toBeDefined();
expect<string>(exportedKey.dq).toBeDefined();
expect<string>(exportedKey.qi).toBeDefined();
});
});
describe('encryptBuffer', () => {
it('should encrypt and decrypt data correctly', async () => {
const key = await generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const plaintext = new TextEncoder().encode('Test encryption');
const iv = getRandomValues(new Uint8Array(12));
const ciphertext = await encryptBuffer(key, { name: 'AES-GCM', iv }, plaintext);
const decrypted = await decryptBuffer(key, { name: 'AES-GCM', iv }, ciphertext);
expect(new TextDecoder().decode(decrypted)).toBe('Test encryption');
});
});
});

View File

@@ -0,0 +1,198 @@
const { subtle } = crypto;
export const randomUUID = () => crypto.randomUUID();
export const getRandomValues = crypto.getRandomValues.bind(crypto);
interface IAesGcmParams extends AesGcmParams {
name: 'AES-GCM';
}
interface IAesCbcParams extends AesCbcParams {
name: 'AES-CBC';
}
interface IAesCtrParams extends AesCtrParams {
name: 'AES-CTR';
}
interface IRsaOaepParams extends RsaOaepParams {
name: 'RSA-OAEP';
}
type ParamsMap = {
'AES-GCM': IAesGcmParams;
'AES-CTR': IAesCtrParams;
'AES-CBC': IAesCbcParams;
'RSA-OAEP': IRsaOaepParams;
};
type ParamsOf<TKey extends IKey> = TKey['algorithm'] extends { name: infer TName extends keyof ParamsMap } ? ParamsMap[TName] : never;
type HasUsage<TKey extends IKey, TUsage extends KeyUsage> = TUsage extends TKey['usages'][number]
? TKey
: TKey & `The provided key cannot be used for ${TUsage}`;
export const encryptBuffer = <TKey extends IKey, TParams extends ParamsOf<TKey>>(
key: HasUsage<TKey, 'encrypt'>,
params: TParams,
data: BufferSource,
): Promise<ArrayBuffer> => subtle.encrypt(params, key, data);
export const decryptBuffer = <TKey extends IKey>(
key: HasUsage<TKey, 'decrypt'>,
params: ParamsOf<TKey>,
data: BufferSource,
): Promise<ArrayBuffer> => subtle.decrypt(params, key, data);
export const deriveBits = (...args: Parameters<typeof subtle.deriveBits>) => subtle.deriveBits(...args);
type AesParams = {
name: 'AES-CBC' | 'AES-GCM' | 'AES-CTR';
length: 128 | 256;
};
type ModeOf<T extends `AES-${'CBC' | 'GCM' | 'CTR'}`> = T extends `AES-${infer Mode}` ? Mode : never;
type Permutations<T> = T extends [infer First]
? [First]
: T extends [infer First, infer Second]
? [First, Second] | [Second, First]
: never;
export interface IKey<
TAlgorithm extends CryptoKey['algorithm'] = CryptoKey['algorithm'],
TExtractable extends CryptoKey['extractable'] = CryptoKey['extractable'],
TType extends CryptoKey['type'] = CryptoKey['type'],
TUsages extends CryptoKey['usages'] = CryptoKey['usages'],
> extends CryptoKey {
readonly algorithm: TAlgorithm;
readonly extractable: TExtractable;
readonly type: TType;
readonly usages: Permutations<TUsages>;
}
export async function generateKey<
const T extends AesParams,
const TExtractable extends boolean = boolean,
const TUsages extends KeyUsage[] = KeyUsage[],
>(algorithm: T, extractable: TExtractable, keyUsages: TUsages): Promise<IKey<T, TExtractable, 'secret', TUsages>> {
const key = await subtle.generateKey(algorithm, extractable, keyUsages);
return key as IKey<T, TExtractable, 'secret', TUsages>;
}
type Filter<TItem extends KeyUsage, TArray extends KeyUsage[], Out extends KeyUsage[] = []> = TArray extends [
infer First extends KeyUsage,
...infer Rest extends KeyUsage[],
]
? First extends TItem
? Filter<TItem, Rest, [...Out, First]>
: Filter<TItem, Rest, Out>
: Out;
export interface IKeyPair<
T extends KeyAlgorithm = KeyAlgorithm,
TExtractable extends boolean = boolean,
TUsages extends KeyUsage[] = KeyUsage[],
> extends CryptoKeyPair {
publicKey: IKey<T, TExtractable, 'public', Filter<'encrypt', TUsages>>;
privateKey: IKey<T, TExtractable, 'private', Filter<'decrypt', TUsages>>;
}
type RsaParams = {
name: 'RSA-OAEP';
modulusLength: 2048;
publicExponent: Uint8Array<ArrayBuffer>;
hash: { name: 'SHA-256' };
};
export async function generateKeyPair<
const T extends RsaParams,
const TExtractable extends boolean = boolean,
const TUsages extends KeyUsage[] = KeyUsage[],
>(algorithm: T, extractable: TExtractable, keyUsages: TUsages): Promise<IKeyPair<T, TExtractable, TUsages>> {
const keyPair = await subtle.generateKey(algorithm, extractable, keyUsages);
return keyPair as IKeyPair<T, TExtractable, TUsages>;
}
type KeyToJwk<T extends IKey> = T['extractable'] extends false
? never
: T['algorithm'] extends AesParams
? {
kty: 'oct';
alg: `A${T['algorithm']['length']}${ModeOf<T['algorithm']['name']>}`;
ext: true;
k: string;
key_ops: T['usages'];
}
: [T['algorithm'], T['type']] extends [RsaParams, 'public']
? {
kty: 'RSA';
alg: 'RSA-OAEP-256';
e: string;
ext: true;
key_ops: T['usages'];
n: string;
}
: [T['algorithm'], T['type']] extends [RsaParams, 'private']
? {
kty: 'RSA';
alg: 'RSA-OAEP-256';
e: string;
ext: true;
d: string;
dp: string;
dq: string;
key_ops: T['usages'];
n: string;
p: string;
q: string;
qi: string;
}
: never;
export type Exported<TFormat extends KeyFormat, TKey extends IKey = IKey> = TFormat extends 'jwk' ? KeyToJwk<TKey> : ArrayBuffer;
export async function exportKey<const TFormat extends KeyFormat = KeyFormat, const TKey extends IKey = IKey>(
format: TFormat,
key: TKey,
): Promise<Exported<TFormat, TKey>> {
const exportedKey = await subtle.exportKey(format, key);
return exportedKey as Exported<TFormat, TKey>;
}
type KtyParams = {
oct: AesParams;
RSA: RsaParams;
};
export async function importJwk<
const TKeyData extends JsonWebKey,
const TAlgorithm extends (TKeyData extends { kty: infer Kty extends keyof KtyParams } ? KtyParams[Kty] : KeyAlgorithm),
const TExtractable extends boolean = boolean,
const TUsages extends KeyUsage[] = KeyUsage[],
const TKeyType extends KeyType = TKeyData extends { kty: 'oct' }
? 'secret'
: TKeyData extends { kty: 'RSA'; key_ops: [infer Op] }
? Op extends 'encrypt'
? 'public'
: 'private'
: KeyType,
>(
jwk: TKeyData,
algorithm: TAlgorithm,
extractable: TExtractable,
keyUsages: TUsages,
): Promise<IKey<TAlgorithm, TExtractable, TKeyType, TUsages>> {
const key = await subtle.importKey('jwk', jwk, algorithm, extractable, keyUsages);
return key as IKey<TAlgorithm, TExtractable, TKeyType, TUsages>;
}
export async function importRaw<
const TAlgorithm extends KeyAlgorithm = KeyAlgorithm,
const TExtractable extends boolean = boolean,
const TUsages extends KeyUsage[] = KeyUsage[],
>(
rawKey: BufferSource,
algorithm: TAlgorithm,
extractable: TExtractable,
keyUsages: TUsages,
): Promise<IKey<TAlgorithm, TExtractable, 'secret', TUsages>> {
const key = await subtle.importKey('raw', rawKey, algorithm, extractable, keyUsages);
return key as IKey<TAlgorithm, TExtractable, 'secret', TUsages>;
}