bastebin/static/js/crypto.js

110 lines
4.0 KiB
JavaScript

'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/<paste_id>#<keyBase64url>
*/
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']
);
},
/**
* 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;