123 lines
4.6 KiB
JavaScript
123 lines
4.6 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']
|
|
);
|
|
},
|
|
|
|
/** 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;
|