aprhodite/static/crypto.js

149 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* crypto.js SexyChat SubtleCrypto wrapper
*
* Provides PBKDF2-derived AES-GCM-256 keys for end-to-end encrypted PMs.
*
* Key facts:
* - Keys are derived deterministically from (password, username) so the
* same credentials always produce the same key across sessions.
* - Keys are marked `extractable: true` so they can be exported as raw bytes
* for the AI transit-encryption flow (sent over HTTPS, never stored).
* - The server only ever receives ciphertext + nonce. It never sees a key
* for user-to-user conversations.
*/
"use strict";
const SexyChato = (() => {
const PBKDF2_ITERATIONS = 100_000;
const KEY_LENGTH_BITS = 256;
const SALT_PREFIX = "sexychat:v1:";
// ── Base64 helpers ────────────────────────────────────────────────────────
function bufToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
}
function base64ToBuf(b64) {
const binary = atob(b64);
const buf = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i);
return buf.buffer;
}
// ── Key derivation ─────────────────────────────────────────────────────────
/**
* Derive an AES-GCM key from a password and username.
* The username acts as a deterministic, per-account salt.
*
* @param {string} password
* @param {string} username
* @returns {Promise<CryptoKey>}
*/
async function deriveKey(password, username) {
const enc = new TextEncoder();
const salt = enc.encode(SALT_PREFIX + username.toLowerCase());
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"],
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: PBKDF2_ITERATIONS,
hash: "SHA-256",
},
baseKey,
{ name: "AES-GCM", length: KEY_LENGTH_BITS },
true, // extractable needed for transit encryption to AI endpoint
["encrypt", "decrypt"],
);
}
// ── Encrypt / decrypt ─────────────────────────────────────────────────────
/**
* Encrypt a plaintext string with an AES-GCM key.
*
* @param {CryptoKey} key
* @param {string} plaintext
* @returns {Promise<{ ciphertext: string, nonce: string }>} both base64
*/
async function encrypt(key, plaintext) {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(plaintext);
const cipherBuf = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
key,
encoded,
);
return {
ciphertext: bufToBase64(cipherBuf),
nonce: bufToBase64(nonce),
};
}
/**
* Decrypt an AES-GCM ciphertext.
*
* @param {CryptoKey} key
* @param {string} ciphertext base64
* @param {string} nonce base64
* @returns {Promise<string>} plaintext
*/
async function decrypt(key, ciphertext, nonce) {
const cipherBuf = base64ToBuf(ciphertext);
const nonceBuf = base64ToBuf(nonce);
const plainBuf = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonceBuf },
key,
cipherBuf,
);
return new TextDecoder().decode(plainBuf);
}
/**
* Export a CryptoKey as a base64-encoded raw byte string.
* Used only for the AI transit flow (sent in POST body over HTTPS,
* never stored by the server).
*
* @param {CryptoKey} key
* @returns {Promise<string>} base64
*/
async function exportKeyBase64(key) {
const raw = await crypto.subtle.exportKey("raw", key);
return bufToBase64(raw);
}
/**
* Import a base64-encoded raw AES-GCM key (e.g. server-provided room key).
*
* @param {string} b64 base64-encoded 256-bit key
* @returns {Promise<CryptoKey>}
*/
async function importKeyBase64(b64) {
const buf = base64ToBuf(b64);
return crypto.subtle.importKey(
"raw",
buf,
{ name: "AES-GCM", length: KEY_LENGTH_BITS },
true,
["encrypt", "decrypt"],
);
}
// ── Public API ─────────────────────────────────────────────────────────────
return { deriveKey, encrypt, decrypt, exportKeyBase64, importKeyBase64 };
})();