132 lines
4.3 KiB
JavaScript
132 lines
4.3 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
|
||
// ── Public API ─────────────────────────────────────────────────────────────
|
||
return { deriveKey, encrypt, decrypt, exportKeyBase64 };
|
||
})();
|