'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); } }; })(); window.PasteCrypto = PasteCrypto;