70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { chacha20poly1305 } from "@noble/ciphers/chacha";
|
|
import { x25519 } from "@noble/curves/ed25519";
|
|
import { hkdf } from "@noble/hashes/hkdf";
|
|
import { sha256 } from "@noble/hashes/sha256";
|
|
import { randomBytes } from "@noble/hashes/utils";
|
|
|
|
const HKDF_SALT = new TextEncoder().encode("backchannel-dm-salt-v1");
|
|
const HKDF_INFO = new TextEncoder().encode("backchannel-dm-key-v1");
|
|
|
|
const encoder = new TextEncoder();
|
|
const decoder = new TextDecoder();
|
|
|
|
export function bytesToBase64(input: Uint8Array): string {
|
|
let bin = "";
|
|
for (const b of input) {
|
|
bin += String.fromCharCode(b);
|
|
}
|
|
return btoa(bin);
|
|
}
|
|
|
|
export function base64ToBytes(input: string): Uint8Array {
|
|
const bin = atob(input);
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i += 1) {
|
|
bytes[i] = bin.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
export function getOrCreateIdentityKey(): string {
|
|
const current = localStorage.getItem("bc.identity_key");
|
|
if (current) {
|
|
return current;
|
|
}
|
|
|
|
const key = bytesToBase64(randomBytes(32));
|
|
localStorage.setItem("bc.identity_key", key);
|
|
return key;
|
|
}
|
|
|
|
export function createEphemeralKeypair(): { privateKey: Uint8Array; publicKeyB64: string } {
|
|
const privateKey = x25519.utils.randomPrivateKey();
|
|
const publicKey = x25519.getPublicKey(privateKey);
|
|
return { privateKey, publicKeyB64: bytesToBase64(publicKey) };
|
|
}
|
|
|
|
export function deriveDmKey(privateKey: Uint8Array, theirPublicKeyB64: string): Uint8Array {
|
|
const theirPublicKey = base64ToBytes(theirPublicKeyB64);
|
|
const sharedSecret = x25519.getSharedSecret(privateKey, theirPublicKey);
|
|
return hkdf(sha256, sharedSecret, HKDF_SALT, HKDF_INFO, 32);
|
|
}
|
|
|
|
export function encryptDm(key: Uint8Array, plaintext: string): { nonce: string; ciphertext: string } {
|
|
const nonceBytes = randomBytes(12);
|
|
const cipher = chacha20poly1305(key, nonceBytes);
|
|
const ciphertext = cipher.encrypt(encoder.encode(plaintext));
|
|
return {
|
|
nonce: bytesToBase64(nonceBytes),
|
|
ciphertext: bytesToBase64(ciphertext),
|
|
};
|
|
}
|
|
|
|
export function decryptDm(key: Uint8Array, nonceB64: string, ciphertextB64: string): string {
|
|
const nonceBytes = base64ToBytes(nonceB64);
|
|
const ciphertext = base64ToBytes(ciphertextB64);
|
|
const cipher = chacha20poly1305(key, nonceBytes);
|
|
const plaintext = cipher.decrypt(ciphertext);
|
|
return decoder.decode(plaintext);
|
|
}
|