'use strict'; /** * PasteCrypto - Client-side AES-GCM 256-bit encryption/decryption * * The encryption key is stored ONLY in the URL fragment (#key). * The fragment is never sent to the server, so the server stores * only opaque ciphertext and has zero knowledge of paste contents. * * Encrypted format: base64url(12-byte IV) + ":" + base64url(ciphertext) * * API usage (programmatic paste creation): * 1. Generate key: await PasteCrypto.generateKey() * 2. Export key: await PasteCrypto.exportKey(key) → base64url string * 3. Encrypt: await PasteCrypto.encrypt(JSON.stringify({title,content,language}), key) * 4. POST to /create: { encrypted_data: "...", expires_in: "never|1hour|1day|1week|1month" } * 5. Share URL: https://yoursite.com/# */ const PasteCrypto = (function () { function arrayBufferToBase64url(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } function base64urlToArrayBuffer(str) { const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4); const binary = atob(padded); const buf = new ArrayBuffer(binary.length); const bytes = new Uint8Array(buf); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return buf; } return { /** Generate a new, random AES-GCM key. Default to 128-bit if not specified. */ async generateKey(length = 128) { return window.crypto.subtle.generateKey( { name: 'AES-GCM', length: length }, true, ['encrypt', 'decrypt'] ); }, /** Export a CryptoKey to a base64url string safe for URL fragments. */ async exportKey(key) { const raw = await window.crypto.subtle.exportKey('raw', key); return arrayBufferToBase64url(raw); }, /** Import a base64url key string into a CryptoKey for decryption. */ async importKey(keyBase64url) { const keyBytes = base64urlToArrayBuffer(keyBase64url); // Support both old 256-bit and new 128-bit keys automatically const keyLength = keyBytes.byteLength * 8; return window.crypto.subtle.importKey( 'raw', keyBytes, { name: 'AES-GCM', length: keyLength }, false, ['decrypt'] ); }, /** Import a base64url key for both encryption AND decryption (used for comment posting). */ async importKeyBidirectional(keyBase64url) { const keyBytes = base64urlToArrayBuffer(keyBase64url); const keyLength = keyBytes.byteLength * 8; return window.crypto.subtle.importKey( 'raw', keyBytes, { name: 'AES-GCM', length: keyLength }, false, ['encrypt', 'decrypt'] ); }, /** * Encrypt a plaintext string. * Returns a string in the format: base64url(iv):base64url(ciphertext) */ async encrypt(plaintext, key) { const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(plaintext); const ciphertext = await window.crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, encoded ); return arrayBufferToBase64url(iv) + ':' + arrayBufferToBase64url(ciphertext); }, /** * Decrypt a string produced by encrypt(). * Returns the original plaintext string. */ async decrypt(encryptedStr, key) { const colonIdx = encryptedStr.indexOf(':'); if (colonIdx === -1) throw new Error('Invalid encrypted data format'); const iv = base64urlToArrayBuffer(encryptedStr.slice(0, colonIdx)); const ciphertext = base64urlToArrayBuffer(encryptedStr.slice(colonIdx + 1)); const decrypted = await window.crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, ciphertext ); return new TextDecoder().decode(decrypted); }, /** * Derive an AES-GCM key from a password using PBKDF2. * Returns { key, saltBase64url } — store the salt in the URL fragment. */ async deriveKeyFromPassword(password, saltBytes = null) { const salt = saltBytes || window.crypto.getRandomValues(new Uint8Array(16)); const enc = new TextEncoder().encode(password); const baseKey = await window.crypto.subtle.importKey( 'raw', enc, 'PBKDF2', false, ['deriveKey'] ); const key = await window.crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 200000, hash: 'SHA-256' }, baseKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); return { key, saltBase64url: arrayBufferToBase64url(salt) }; }, /** * Re-derive a key from a password and a known salt (base64url string). */ async deriveKeyFromPasswordAndSalt(password, saltBase64url) { const salt = new Uint8Array(base64urlToArrayBuffer(saltBase64url)); const { key } = await this.deriveKeyFromPassword(password, salt); return key; } }; })(); window.PasteCrypto = PasteCrypto;