client/packages/crypto/src/chatSecret.ts
Alois 8ddc8db536
Some checks failed
/ build-web (push) Successful in 7m21s
/ build-desktop (linux) (push) Failing after 7m49s
/ build-mobile (push) Successful in 20m5s
/ release (push) Has been skipped
(wip): fix chat crypto
2026-07-06 17:52:41 +02:00

153 lines
3.8 KiB
TypeScript

import { base64ToBytes, bytesToBase64, crypto } from "mtp";
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export const CHAT_SECRET_WRAPPING_SCHEME =
"mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1";
const CHAT_SECRET_SALT = textEncoder.encode("tensamin-chat-secret-v1");
const CHAT_MESSAGE_SALT = textEncoder.encode("tensamin-chat-message-v1");
export function deriveChatId(ownUserId: number, peerUserId: number): string {
const ids = [ownUserId, peerUserId].sort((a, b) => a - b);
return `${ids[0]}:${ids[1]}`;
}
export function deriveChatSecretId(chatId: string): string {
return `chat:${chatId}:main`;
}
export function randomChatSecret(): Uint8Array {
return globalThis.crypto.getRandomValues(new Uint8Array(32));
}
export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array {
return crypto.keyringToKeys(keyring).kemPublicKey;
}
export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array {
return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey;
}
export async function wrapChatSecret(args: {
chatSecret: Uint8Array;
recipientKemPublicKey: Uint8Array;
chatId: string;
secretId: string;
version: number;
}): Promise<{
encryptedSecret: Uint8Array;
kemCiphertext: Uint8Array;
wrappingScheme: string;
}> {
const enc = crypto.encapsulate(args.recipientKemPublicKey);
try {
const wrappingKey = deriveWrappingKey({
sharedSecret: enc.shared_secret,
chatId: args.chatId,
secretId: args.secretId,
version: args.version,
});
try {
return {
encryptedSecret: await crypto.encrypt(wrappingKey, args.chatSecret),
kemCiphertext: enc.ciphertext,
wrappingScheme: CHAT_SECRET_WRAPPING_SCHEME,
};
} finally {
wrappingKey.fill(0);
}
} finally {
enc.shared_secret.fill(0);
}
}
export async function unwrapChatSecret(args: {
encryptedSecret: Uint8Array;
kemCiphertext: Uint8Array;
keyring: string;
chatId: string;
secretId: string;
version: number;
wrappingScheme: string;
}): Promise<Uint8Array> {
if (args.wrappingScheme !== CHAT_SECRET_WRAPPING_SCHEME) {
throw new Error(
`Unsupported chat secret wrapping scheme: ${args.wrappingScheme}`,
);
}
const ownKeys = crypto.keyringToKeys(args.keyring);
const sharedSecret = crypto.decapsulate(
ownKeys.kemSecretKey,
args.kemCiphertext,
);
try {
const wrappingKey = deriveWrappingKey({
sharedSecret,
chatId: args.chatId,
secretId: args.secretId,
version: args.version,
});
try {
return await crypto.decrypt(wrappingKey, args.encryptedSecret);
} finally {
wrappingKey.fill(0);
}
} finally {
sharedSecret.fill(0);
}
}
export async function encryptChatText(
chatSecret: Uint8Array,
plaintext: string,
): Promise<string> {
const key = deriveMessageKey(chatSecret);
try {
return bytesToBase64(
await crypto.encrypt(key, textEncoder.encode(plaintext)),
);
} finally {
key.fill(0);
}
}
export async function decryptChatText(
chatSecret: Uint8Array,
ciphertext: string,
): Promise<string> {
const key = deriveMessageKey(chatSecret);
try {
return textDecoder.decode(
await crypto.decrypt(key, base64ToBytes(ciphertext)),
);
} finally {
key.fill(0);
}
}
function deriveWrappingKey(args: {
sharedSecret: Uint8Array;
chatId: string;
secretId: string;
version: number;
}): Uint8Array {
return crypto.deriveEncryptionKey(
args.sharedSecret,
CHAT_SECRET_SALT,
textEncoder.encode(`${args.chatId}:${args.secretId}:${args.version}`),
);
}
function deriveMessageKey(chatSecret: Uint8Array): Uint8Array {
return crypto.deriveEncryptionKey(
chatSecret,
CHAT_MESSAGE_SALT,
textEncoder.encode("message-content"),
);
}