Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Failing after 3m32s
/ build-desktop (linux) (push) Failing after 2m47s
/ build-mobile (push) Failing after 5m29s
/ release (push) Has been skipped
117 lines
2.8 KiB
TypeScript
117 lines
2.8 KiB
TypeScript
import { crypto } from "mtp";
|
|
|
|
const textEncoder = new TextEncoder();
|
|
const textDecoder = new TextDecoder();
|
|
|
|
export const CALL_SECRET_WRAPPING_SCHEME =
|
|
"mtp-call-secret-kem-chacha20poly1305-hkdf-sha256-v1";
|
|
|
|
const CALL_SECRET_SALT = textEncoder.encode("tensamin-call-secret-v1");
|
|
|
|
export function deriveCallSecretId(callId: string): string {
|
|
return `call:${callId}:main`;
|
|
}
|
|
|
|
export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array {
|
|
return crypto.keyringToKeys({ value: keyring, encoding: "base64" })
|
|
.kemPublicKey;
|
|
}
|
|
|
|
export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array {
|
|
return crypto.publicKeyBundleToKeys({ value: publicKey, encoding: "base64" })
|
|
.kemPublicKey;
|
|
}
|
|
|
|
export async function wrapCallSecret(args: {
|
|
callSecret: string;
|
|
recipientKemPublicKey: Uint8Array;
|
|
callId: 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,
|
|
callId: args.callId,
|
|
secretId: args.secretId,
|
|
version: args.version,
|
|
});
|
|
|
|
try {
|
|
return {
|
|
encryptedSecret: await crypto.encrypt(
|
|
wrappingKey,
|
|
textEncoder.encode(args.callSecret),
|
|
),
|
|
kemCiphertext: enc.ciphertext,
|
|
wrappingScheme: CALL_SECRET_WRAPPING_SCHEME,
|
|
};
|
|
} finally {
|
|
wrappingKey.fill(0);
|
|
}
|
|
} finally {
|
|
enc.shared_secret.fill(0);
|
|
}
|
|
}
|
|
|
|
export async function unwrapCallSecret(args: {
|
|
encryptedSecret: Uint8Array;
|
|
kemCiphertext: Uint8Array;
|
|
keyring: string;
|
|
callId: string;
|
|
secretId: string;
|
|
version: number;
|
|
wrappingScheme: string;
|
|
}): Promise<string> {
|
|
if (args.wrappingScheme !== CALL_SECRET_WRAPPING_SCHEME) {
|
|
throw new Error(
|
|
`Unsupported call secret wrapping scheme: ${args.wrappingScheme}`,
|
|
);
|
|
}
|
|
|
|
const ownKeys = crypto.keyringToKeys({
|
|
value: args.keyring,
|
|
encoding: "base64",
|
|
});
|
|
const sharedSecret = crypto.decapsulate(
|
|
ownKeys.kemSecretKey,
|
|
args.kemCiphertext,
|
|
);
|
|
|
|
try {
|
|
const wrappingKey = deriveWrappingKey({
|
|
sharedSecret,
|
|
callId: args.callId,
|
|
secretId: args.secretId,
|
|
version: args.version,
|
|
});
|
|
|
|
try {
|
|
return textDecoder.decode(
|
|
await crypto.decrypt(wrappingKey, args.encryptedSecret),
|
|
);
|
|
} finally {
|
|
wrappingKey.fill(0);
|
|
}
|
|
} finally {
|
|
sharedSecret.fill(0);
|
|
}
|
|
}
|
|
|
|
function deriveWrappingKey(args: {
|
|
sharedSecret: Uint8Array;
|
|
callId: string;
|
|
secretId: string;
|
|
version: number;
|
|
}): Uint8Array {
|
|
return crypto.deriveEncryptionKey(
|
|
args.sharedSecret,
|
|
CALL_SECRET_SALT,
|
|
textEncoder.encode(`${args.callId}:${args.secretId}:${args.version}`),
|
|
);
|
|
}
|