fix: improve JS security and error handling in html.gleam
This commit is contained in:
119
src/html.gleam
119
src/html.gleam
@@ -121,6 +121,11 @@ fn decrypt_js(encrypted_content: String) -> String {
|
||||
content: document.getElementById('content-display')
|
||||
};
|
||||
|
||||
if (!els.loading || !els.error || !els.content) {
|
||||
console.error('Required DOM elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hash) {
|
||||
els.loading.classList.add('hidden');
|
||||
els.error.classList.remove('hidden');
|
||||
@@ -128,7 +133,9 @@ fn decrypt_js(encrypted_content: String) -> String {
|
||||
}
|
||||
|
||||
try {
|
||||
const keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
|
||||
let keyBase64 = hash.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (keyBase64.length % 4) keyBase64 += '=';
|
||||
|
||||
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);
|
||||
@@ -150,70 +157,106 @@ fn decrypt_js(encrypted_content: String) -> String {
|
||||
}
|
||||
|
||||
fn escape_js_string(s: String) -> String {
|
||||
s
|
||||
|> string.replace("\\", "\\\\")
|
||||
|> string.replace("'", "\\'")
|
||||
|> string.replace("\"", "\\\"")
|
||||
|> string.replace("\n", "\\n")
|
||||
|> string.replace("\r", "\\r")
|
||||
let escaped =
|
||||
s
|
||||
|> string.replace("\\", "\\\\")
|
||||
|> string.replace("'", "\\'")
|
||||
|> string.replace("\"", "\\\"")
|
||||
|> string.replace("\n", "\\n")
|
||||
|> string.replace("\r", "\\r")
|
||||
|> string.replace("<", "\\u003c")
|
||||
|> string.replace(">", "\\u003e")
|
||||
|> string.replace("&", "\\u0026")
|
||||
|
||||
escaped
|
||||
}
|
||||
|
||||
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 iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data);
|
||||
const keyData = await crypto.subtle.exportKey('raw', key);
|
||||
return {
|
||||
encrypted: btoa(String.fromCharCode(...new Uint8Array([...iv, ...new Uint8Array(encrypted)]))),
|
||||
key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
|
||||
};
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(content);
|
||||
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 }, key, data);
|
||||
const keyData = await crypto.subtle.exportKey('raw', key);
|
||||
return {
|
||||
encrypted: btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted))),
|
||||
key: btoa(String.fromCharCode(...new Uint8Array(keyData)))
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Encryption failed:', e);
|
||||
throw new Error('Failed to encrypt content');
|
||||
}
|
||||
}
|
||||
|
||||
function base64ToUrlSafe(base64) {
|
||||
return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
||||
return base64.split('+').join('-').split('/').join('_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function showShareUrl(url) {
|
||||
const card = document.querySelector('.card');
|
||||
if (!card) {
|
||||
console.error('Card element not found');
|
||||
return;
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.className = 'share-url';
|
||||
div.innerHTML = '<label>Share this URL</label>';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = 'Share this URL';
|
||||
div.appendChild(label);
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.readOnly = true;
|
||||
input.value = url;
|
||||
input.onclick = () => input.select();
|
||||
input.addEventListener('click', () => input.select());
|
||||
div.appendChild(input);
|
||||
card.insertBefore(div, card.firstChild);
|
||||
input.select();
|
||||
}
|
||||
|
||||
document.getElementById('paste-form').addEventListener('submit', async (e) => {
|
||||
document.getElementById('paste-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const content = document.getElementById('content').value;
|
||||
const { encrypted, key } = await encryptContent(content);
|
||||
document.getElementById('encrypted-content').value = encrypted;
|
||||
const csrfToken = document.getElementById('csrf-token').value;
|
||||
|
||||
const submitButton = e.target.querySelector('button[type=\"submit\"]');
|
||||
const contentInput = document.getElementById('content');
|
||||
const encryptedInput = document.getElementById('encrypted-content');
|
||||
const csrfInput = document.getElementById('csrf-token');
|
||||
|
||||
if (!contentInput || !encryptedInput || !csrfInput) {
|
||||
console.error('Required form elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (submitButton) submitButton.disabled = true;
|
||||
|
||||
try {
|
||||
const content = contentInput.value;
|
||||
const { encrypted, key } = await encryptContent(content);
|
||||
encryptedInput.value = encrypted;
|
||||
const csrfToken = csrfInput.value;
|
||||
|
||||
const res = await fetch('/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
|
||||
});
|
||||
const res = await fetch('/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `encrypted_content=${encodeURIComponent(encrypted)}&csrf_token=${encodeURIComponent(csrfToken)}`
|
||||
});
|
||||
|
||||
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 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) throw new Error('No paste ID returned');
|
||||
|
||||
const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
|
||||
document.body.replaceChildren(...doc.body.childNodes);
|
||||
showShareUrl(pasteUrl);
|
||||
history.replaceState({}, '', pasteUrl);
|
||||
const pasteUrl = `${location.origin}/paste/${pasteId}#${base64ToUrlSafe(key)}`;
|
||||
document.body.replaceChildren(...doc.body.childNodes);
|
||||
showShareUrl(pasteUrl);
|
||||
history.replaceState({}, '', pasteUrl);
|
||||
} catch (err) {
|
||||
console.error('Form submission failed:', err);
|
||||
alert('Failed to create paste. Please try again.');
|
||||
if (submitButton) submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user