(feat): more crypto migration
Some checks failed
/ build-web (push) Failing after 5m33s
/ build-desktop (linux) (push) Failing after 5m46s
/ build-mobile (push) Failing after 7m58s
/ release (push) Has been skipped

This commit is contained in:
Alois 2026-07-06 00:13:26 +02:00
commit 2777ba34ca
11 changed files with 504 additions and 715 deletions

View file

@ -0,0 +1,143 @@
import { crypto } from "mtp";
const textEncoder = new TextEncoder();
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 await crypto.encryptText(key, plaintext);
} finally {
key.fill(0);
}
}
export async function decryptChatText(
chatSecret: Uint8Array,
ciphertext: string,
): Promise<string> {
const key = deriveMessageKey(chatSecret);
try {
return await crypto.decryptText(key, 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"),
);
}