Files
upstream-rocketchat/apps/meteor/client/lib/e2ee/keychain.ts
upstream-mirror 30810eecd7
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
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

164 lines
4.3 KiB
TypeScript

import { Base64 } from '@rocket.chat/base64';
import { Binary } from './binary';
import type { ICodec } from './codec';
import * as Pbkdf2 from './crypto/pbkdf2';
import { randomUUID } from './crypto/shared';
/**
* Version 1 format:
* ```
* json({ $binary: base64(iv[16] + ciphertext) })
* ```
*/
interface IStoredKeyV1 {
/**
* Base64-encoded binary data
* - first 16 bytes are the IV
* - remaining bytes are the ciphertext
*/
$binary: string;
}
/**
* Version 2 format:
* ```typescript
* json({ iv: base64(iv[12]), ciphertext: base64(data[...]), salt: string(), iterations: number() })
* ```
*/
interface IStoredKeyV2 {
iv: string;
ciphertext: string;
salt: string;
iterations: number;
}
type StoredKey = IStoredKeyV1 | IStoredKeyV2;
// eslint-disable-next-line @typescript-eslint/no-redeclare
const StoredKey: ICodec<string, StoredKey> = {
decode: (data) => {
const json: unknown = JSON.parse(data);
if (typeof json !== 'object' || json === null) {
throw new TypeError('Invalid private key format');
}
if ('$binary' in json && typeof json.$binary === 'string') {
return { $binary: json.$binary } satisfies IStoredKeyV1;
}
if (
'iv' in json &&
typeof json.iv === 'string' &&
'ciphertext' in json &&
typeof json.ciphertext === 'string' &&
'salt' in json &&
typeof json.salt === 'string' &&
'iterations' in json &&
typeof json.iterations === 'number'
) {
return { iv: json.iv, ciphertext: json.ciphertext, salt: json.salt, iterations: json.iterations } satisfies IStoredKeyV2;
}
throw new TypeError('Invalid private key format');
},
encode: (data) => JSON.stringify(data),
};
type EncryptedKeyContent = {
iv: Uint8Array<ArrayBuffer>;
ciphertext: Uint8Array<ArrayBuffer>;
};
type EncryptedKeyOptions = {
salt: string;
iterations: number;
};
type EncryptedKey = {
content: EncryptedKeyContent;
options: EncryptedKeyOptions;
};
class EncryptedKeyCodec implements ICodec<string, EncryptedKey, IStoredKeyV2> {
userId: string;
constructor(userId: string) {
this.userId = userId;
}
encode(encryptedKey: EncryptedKey): IStoredKeyV2 {
return {
iv: Base64.encode(encryptedKey.content.iv),
ciphertext: Base64.encode(encryptedKey.content.ciphertext),
salt: encryptedKey.options.salt,
iterations: encryptedKey.options.iterations,
};
}
decode(storedKey: string): EncryptedKey {
const storedKeyObj = StoredKey.decode(storedKey);
if ('$binary' in storedKeyObj) {
// v1
const binary = Base64.decode(storedKeyObj.$binary);
return {
content: { iv: binary.slice(0, 16), ciphertext: binary.slice(16) },
options: {
salt: this.userId,
iterations: 1000,
},
};
}
// v2
const { iv, ciphertext, salt, iterations } = storedKeyObj;
return {
content: {
iv: Base64.decode(iv),
ciphertext: Base64.decode(ciphertext),
},
options: {
salt,
iterations,
},
};
}
}
export class Keychain {
private readonly userId: string;
private readonly codec: EncryptedKeyCodec;
constructor(userId: string) {
this.userId = userId;
this.codec = new EncryptedKeyCodec(userId);
}
async decryptKey(privateKey: string, password: string): Promise<string> {
const { content, options } = this.codec.decode(privateKey);
const algorithm = content.iv.length === 16 ? 'AES-CBC' : 'AES-GCM';
const baseKey = await Pbkdf2.importBaseKey(new Uint8Array(Binary.decode(password)));
const derivedBits = await Pbkdf2.derive(baseKey, {
salt: new Uint8Array(Binary.decode(options.salt)),
iterations: options.iterations,
});
const key = await Pbkdf2.importKey(derivedBits, { name: algorithm, length: 256 });
const decrypted = await Pbkdf2.decrypt(key, content);
return Binary.encode(decrypted.buffer);
}
async encryptKey(privateKey: string, password: string): Promise<IStoredKeyV2> {
const salt = `v2:${this.userId}:${randomUUID()}`;
const iterations = 100_000;
const algorithm = 'AES-GCM';
const baseKey = await Pbkdf2.importBaseKey(new Uint8Array(Binary.decode(password)));
const derivedBits = await Pbkdf2.derive(baseKey, { salt: new Uint8Array(Binary.decode(salt)), iterations });
const key = await Pbkdf2.importKey(derivedBits, { name: algorithm, length: 256 });
const content = await Pbkdf2.encrypt(key, new Uint8Array(Binary.decode(privateKey)));
return this.codec.encode({ content, options: { salt, iterations } });
}
}