diff --git a/script.js b/script.js new file mode 100644 index 0000000..7d67953 --- /dev/null +++ b/script.js @@ -0,0 +1,255 @@ +let pendingKey = null; +let pendingData = null; + +function showError(msg) { + document.getElementById('errorDisplay').style.display = 'flex'; + document.getElementById('errorMessage').textContent = msg; +} + +function clearError() { + document.getElementById('errorDisplay').style.display = 'none'; +} + +function isRTL(text) { + const rtlRegex = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF\uFB50-\uFDFF\uFE70-\uFEFF]/; + return rtlRegex.test(text); +} + +const contentTextarea = document.getElementById('content'); +if (contentTextarea) { + contentTextarea.addEventListener('input', function(e) { + const text = e.target.value; + if (isRTL(text)) { + e.target.style.direction = 'rtl'; + e.target.style.fontFamily = "'Vazirmatn', sans-serif"; + } else { + e.target.style.direction = 'ltr'; + e.target.style.fontFamily = "'JetBrains Mono', monospace"; + } + }); +} + +function togglePassword() { + const enabled = document.getElementById('enablePassword').checked; + const wrapper = document.getElementById('passwordWrapper'); + wrapper.classList.toggle('show', enabled); +} + +const Base64 = { + encode(buf) { + const bytes = new Uint8Array(buf); + let bin = ''; + for (let b of bytes) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + }, + decode(str) { + str = str.replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) str += '='; + const bin = atob(str); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return bytes; + } +}; + +async function deriveKeyFromPassword(password, salt) { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'] + ); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); +} + +function genId() { + const arr = new Uint8Array(16); + crypto.getRandomValues(arr); + return Array.from(arr, b => b.toString(16).padStart(2, '0')).join(''); +} + +async function createPaste() { + clearError(); + const content = document.getElementById('content').value.trim(); + if (!content) return showError('Please enter content to encrypt'); + + const hasPassword = document.getElementById('enablePassword').checked; + const password = document.getElementById('passwordInput').value; + + if (hasPassword && password.length < 4) { + return showError('Password must be at least 4 characters'); + } + + const btn = document.getElementById('createBtn'); + const btnText = document.getElementById('btnText'); + btn.disabled = true; + btnText.innerHTML = ' Encrypting...'; + + try { + let key, keyToExport; + + if (hasPassword) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + key = await deriveKeyFromPassword(password, salt); + keyToExport = salt; + } else { + key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); + keyToExport = await crypto.subtle.exportKey('raw', key); + } + + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + new TextEncoder().encode(content) + ); + + const id = genId(); + const payload = { + iv: Array.from(iv), + data: Array.from(new Uint8Array(encrypted)) + }; + + const res = await fetch('/api/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id, + encryptedData: payload, + expiresIn: parseInt(document.getElementById('expiresIn').value), + burnAfterRead: document.getElementById('burnAfterRead').checked, + hasPassword: hasPassword + }) + }); + + const result = await res.json(); + if (!res.ok) throw new Error(result.error || 'Server error'); + + let url; + if (hasPassword) { + url = `${location.origin}/#${id}:${Base64.encode(keyToExport)}:pwd`; + } else { + url = `${location.origin}/#${id}:${Base64.encode(keyToExport)}`; + } + + document.getElementById('shareUrl').value = url; + document.getElementById('expiryTime').textContent = new Date(Date.now() + parseInt(document.getElementById('expiresIn').value) * 1000).toLocaleString(); + document.getElementById('passwordNotice').style.display = hasPassword ? 'flex' : 'none'; + document.getElementById('resultBox').classList.add('show'); + document.getElementById('content').value = ''; + + } catch (err) { + showError(err.message); + } finally { + btn.disabled = false; + btnText.textContent = '🔐 Encrypt & Save'; + } +} + +async function decryptPaste() { + const hash = location.hash.slice(1); + if (!hash.includes(':')) return; + + const parts = hash.split(':'); + const id = parts[0]; + const keyData = parts[1]; + const isPasswordProtected = parts[2] === 'pwd'; + + try { + const res = await fetch(`/api/get/${id}`); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + + if (data.hasPassword || isPasswordProtected) { + pendingKey = keyData; + pendingData = data; + document.getElementById('createView').style.display = 'none'; + document.getElementById('passwordPrompt').classList.add('show'); + return; + } + + await performDecryption(keyData, data); + + } catch (err) { + showError('Failed: ' + err.message); + setTimeout(() => location.href = '/', 3000); + } +} + +async function decryptWithPassword() { + const password = document.getElementById('decryptPassword').value; + if (!password) return; + + const btn = document.querySelector('#passwordPrompt .btn'); + const btnText = document.getElementById('decryptBtnText'); + btn.disabled = true; + btnText.innerHTML = ' Decrypting...'; + + try { + const salt = Base64.decode(pendingKey); + const key = await deriveKeyFromPassword(password, salt); + await performDecryption(key, pendingData, true); + } catch (err) { + document.getElementById('passwordError').style.display = 'block'; + btn.disabled = false; + btnText.textContent = '🔓 Decrypt'; + } +} + +async function performDecryption(keyOrData, data, isKeyObject = false) { + let key; + + if (isKeyObject) { + key = keyOrData; + } else { + const keyRaw = Base64.decode(keyOrData); + key = await crypto.subtle.importKey( + 'raw', keyRaw, { name: 'AES-GCM', length: 256 }, false, ['decrypt'] + ); + } + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: new Uint8Array(data.data.iv) }, + key, + new Uint8Array(data.data.data) + ); + + const text = new TextDecoder().decode(decrypted); + + document.getElementById('passwordPrompt').classList.remove('show'); + document.getElementById('decryptView').classList.add('show'); + + if (data.burnAfterRead) { + document.getElementById('burnNotice').style.display = 'flex'; + } + + const contentBox = document.getElementById('decryptedContent'); + contentBox.textContent = text; + + if (isRTL(text)) { + contentBox.style.direction = 'rtl'; + contentBox.style.fontFamily = "'Vazirmatn', sans-serif"; + } else { + contentBox.style.direction = 'ltr'; + contentBox.style.fontFamily = "'JetBrains Mono', monospace"; + } + + history.replaceState(null, null, ' '); +} + +function copyUrl() { + const inp = document.getElementById('shareUrl'); + inp.select(); + document.execCommand('copy'); + const btn = document.querySelector('.btn-copy'); + btn.textContent = '✅ Copied!'; + setTimeout(() => btn.textContent = '📋 Copy', 2000); +} + +window.addEventListener('load', () => { + if (location.hash.length > 1) decryptPaste(); +});