diff --git a/src/html.gleam b/src/html.gleam
index 44460c6..16582de 100644
--- a/src/html.gleam
+++ b/src/html.gleam
@@ -112,47 +112,38 @@ pub fn not_found() -> String {
fn decrypt_js(encrypted_content: String) -> String {
"
-(async function() {
+(async () => {
const encryptedContent = '" <> escape_js_string(encrypted_content) <> "';
- const hash = window.location.hash.slice(1);
- const loadingEl = document.getElementById('loading');
- const errorEl = document.getElementById('error');
- const contentEl = document.getElementById('content-display');
-
+ const hash = location.hash.slice(1);
+ const els = {
+ loading: document.getElementById('loading'),
+ error: document.getElementById('error'),
+ content: document.getElementById('content-display')
+ };
+
if (!hash) {
- loadingEl.classList.add('hidden');
- errorEl.classList.remove('hidden');
+ els.loading.classList.add('hidden');
+ els.error.classList.remove('hidden');
return;
}
-
+
try {
const keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
const encryptedBytes = Uint8Array.from(atob(encryptedContent), c => c.charCodeAt(0));
const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const iv = encryptedBytes.slice(0, 12);
- const ciphertext = encryptedBytes.slice(12);
- const key = await crypto.subtle.importKey(
- 'raw',
- keyBytes,
- { name: 'AES-GCM' },
- false,
- ['decrypt']
- );
- const decrypted = await crypto.subtle.decrypt(
- { name: 'AES-GCM', iv: iv },
- key,
- ciphertext
- );
- const decoder = new TextDecoder();
- const plaintext = decoder.decode(decrypted);
- contentEl.textContent = plaintext;
- loadingEl.classList.add('hidden');
- errorEl.classList.add('hidden');
- contentEl.classList.remove('hidden');
+
+ const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']);
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, encryptedBytes.slice(12));
+
+ els.content.textContent = new TextDecoder().decode(decrypted);
+ els.loading.classList.add('hidden');
+ els.error.classList.add('hidden');
+ els.content.classList.remove('hidden');
} catch (e) {
console.error('Decryption failed:', e);
- loadingEl.classList.add('hidden');
- errorEl.classList.remove('hidden');
+ els.loading.classList.add('hidden');
+ els.error.classList.remove('hidden');
}
})();
"
@@ -172,73 +163,57 @@ fn crypto_js() -> String {
async function encryptContent(content) {
const encoder = new TextEncoder();
const data = encoder.encode(content);
- const key = await crypto.subtle.generateKey(
- { name: 'AES-GCM', length: 256 },
- true,
- ['encrypt', 'decrypt']
- );
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
- const encrypted = await crypto.subtle.encrypt(
- { name: 'AES-GCM', iv: iv },
- key,
- data
- );
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
const keyData = await crypto.subtle.exportKey('raw', key);
- const keyBytes = new Uint8Array(keyData);
- const combined = new Uint8Array(iv.length + encrypted.byteLength);
- combined.set(iv);
- combined.set(new Uint8Array(encrypted), iv.length);
- const encryptedBase64 = btoa(String.fromCharCode(...combined));
- const keyBase64 = btoa(String.fromCharCode(...keyBytes));
- return { encrypted: encryptedBase64, key: keyBase64 };
+ return {
+ encrypted: btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(encrypted)]))),
+ key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
+ };
}
-async function base64ToUrlSafeBase64(base64) {
- let result = base64.split('+').join('-');
- result = result.split('/').join('_');
- while (result.endsWith('=')) {
- result = result.slice(0, -1);
- }
- return result;
+function base64ToUrlSafe(base64) {
+ return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
}
-document.getElementById('paste-form').addEventListener('submit', async function(e) {
+function showShareUrl(url) {
+ const card = document.querySelector('.card');
+ const div = document.createElement('div');
+ div.className = 'share-url';
+ div.innerHTML = '';
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.readOnly = true;
+ input.value = url;
+ input.onclick = () => input.select();
+ div.appendChild(input);
+ card.insertBefore(div, card.firstChild);
+ input.select();
+}
+
+document.getElementById('paste-form').addEventListener('submit', async (e) => {
e.preventDefault();
const content = document.getElementById('content').value;
- const result = await encryptContent(content);
- document.getElementById('encrypted-content').value = result.encrypted;
- const response = await fetch('/', {
+ const { encrypted, key } = await encryptContent(content);
+ document.getElementById('encrypted-content').value = encrypted;
+ const csrfToken = document.getElementById('csrf-token').value;
+
+ const res = await fetch('/', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: 'encrypted_content=' + encodeURIComponent(result.encrypted) + '&csrf_token=' + encodeURIComponent(document.getElementById('csrf-token').value)
+ body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
});
- const html = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const keyUrlSafe = await base64ToUrlSafeBase64(result.key);
- const url = new URL(window.location.href);
- const pasteId = doc.querySelector('[data-paste-id]').getAttribute('data-paste-id');
- const pasteUrl = url.origin + '/paste/' + pasteId + '#' + keyUrlSafe;
-
+
+ const html = await res.text();
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const pasteId = doc.querySelector('[data-paste-id]')?.getAttribute('data-paste-id');
+ if (!pasteId) return;
+
+ const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
document.body.replaceChildren(...doc.body.childNodes);
-
- const card = document.querySelector('.card');
- const urlDiv = document.createElement('div');
- urlDiv.className = 'share-url';
- const lbl = document.createElement('label');
- lbl.textContent = 'Share this URL';
- urlDiv.appendChild(lbl);
- const inp = document.createElement('input');
- inp.type = 'text';
- inp.readOnly = true;
- inp.value = pasteUrl;
- inp.onclick = function() { this.select(); };
- urlDiv.appendChild(inp);
- card.insertBefore(urlDiv, card.firstChild);
-
- window.history.replaceState({}, '', pasteUrl);
-
- inp.select();
+ showShareUrl(pasteUrl);
+ history.replaceState({}, '', pasteUrl);
});
"
}