PRIVACY
TEXT ENCRYPT
Encrypt any text into a shareable link. Password-lockable. AES-256, browser-only — nothing stored anywhere.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// ── b64url ─────────────────────────────────────────────────────────────────
function toB64(bytes) {
return btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function fromB64(str) {
return Uint8Array.from(atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
}
// ── Compression ────────────────────────────────────────────────────────────
async function compress(bytes, algo) {
if (algo === 'none') return bytes;
const cs = new CompressionStream(algo === 'gzip' ? 'gzip' : 'deflate-raw');
const w = cs.writable.getWriter();
w.write(bytes); w.close();
return new Uint8Array(await new Response(cs.readable).arrayBuffer());
}
async function decompress(bytes, algo) {
if (algo === 'none') return bytes;
const ds = new DecompressionStream(algo === 'gzip' ? 'gzip' : 'deflate-raw');
const w = ds.writable.getWriter();
w.write(bytes); w.close();
return new Uint8Array(await new Response(ds.readable).arrayBuffer());
}
// ── AES-GCM key mode ───────────────────────────────────────────────────────
async function encryptKey(plain) {
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain);
const raw = await crypto.subtle.exportKey('raw', key);
return { a: raw, b: iv, c: new Uint8Array(ct) };
}
async function decryptKey(a, b, c) {
const key = await crypto.subtle.importKey('raw', a, { name: 'AES-GCM' }, false, ['decrypt']);
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: b }, key, c));
}
// ── AES-GCM password / PBKDF2 mode ────────────────────────────────────────
async function deriveKey(password, salt) {
const km = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveKey']);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 200_000, hash: 'SHA-256' },
km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
);
}
async function encryptPassword(plain, password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain);
return { a: salt, b: iv, c: new Uint8Array(ct) };
}
async function decryptPassword(a, b, c, password) {
const key = await deriveKey(password, a);
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: b }, key, c));
}
// ── Fragment format ────────────────────────────────────────────────────────
// mode: k=key-in-url n=4-digit-pin a=alphanumeric
// comp suffix: (none) d=deflate g=gzip
// #<mode>[comp].<partA>.<partB>.<partC>
function buildFrag(mode, comp, { a, b, c }) {
const cs = comp === 'deflate' ? 'd' : comp === 'gzip' ? 'g' : '';
return `${mode}${cs}.${toB64(a)}.${toB64(b)}.${toB64(c)}`;
}
function parseFrag(frag) {
const dot = frag.indexOf('.');
if (dot === -1) return null;
const flag = frag.slice(0, dot);
const parts = frag.slice(dot + 1).split('.');
if (parts.length !== 3) return null;
const mode = flag[0]; // k | n | a
const comp = flag[1] === 'd' ? 'deflate' : flag[1] === 'g' ? 'gzip' : 'none';
if (!['k', 'n', 'a'].includes(mode)) return null;
return { mode, comp, a: fromB64(parts[0]), b: fromB64(parts[1]), c: fromB64(parts[2]) };
}
// ── Encrypt side state ─────────────────────────────────────────────────────
let pwType = 'none'; // none | pin | alpha
let compMode = 'none'; // none | deflate | gzip
let parsedFrag = null;
// ── DOM refs ───────────────────────────────────────────────────────────────
const textInput = document.getElementById('text-input');
const encryptBtn = document.getElementById('encrypt-btn');
const clearBtn = document.getElementById('clear-btn');
const encryptError = document.getElementById('encrypt-error');
const resultSection = document.getElementById('result-section');
const shareLink = document.getElementById('share-link');
const copyBtn = document.getElementById('copy-btn');
const resultBadges = document.getElementById('result-badges');
const encryptSection = document.getElementById('encrypt-section');
const decryptSection = document.getElementById('decrypt-section');
const decryptedContent = document.getElementById('decrypted-content');
const decryptMeta = document.getElementById('decrypt-meta');
const copyDecryptedBtn = document.getElementById('copy-decrypted-btn');
const decryptError = document.getElementById('decrypt-error');
// modal
const unlockModal = document.getElementById('unlock-modal');
const modalBox = document.getElementById('modal-box');
const modalSubtitle = document.getElementById('modal-subtitle');
const modalLabel = document.getElementById('modal-input-label');
const modalPinRow = document.getElementById('modal-pin-row');
const modalPwWrap = document.getElementById('modal-pw-wrap');
const modalPwInput = document.getElementById('modal-pw-input');
const modalPwToggle = document.getElementById('modal-pw-toggle');
const modalUnlockBtn = document.getElementById('modal-unlock-btn');
const modalError = document.getElementById('modal-error');
const modalPinDigits = Array.from(document.querySelectorAll('.modal-pin-digit'));
// encrypt-side
const pwNoneHint = document.getElementById('pw-none-hint');
const pwPinBlock = document.getElementById('pw-pin-block');
const pwAlphaBlock = document.getElementById('pw-alpha-block');
const pwAlphaInput = document.getElementById('pw-alpha-input');
const pwAlphaToggle= document.getElementById('pw-alpha-toggle');
const encPinDigits = Array.from(document.querySelectorAll('.pin-digit'));
// ── Password type tabs ─────────────────────────────────────────────────────
document.querySelectorAll('.pw-type-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.pw-type-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
pwType = tab.dataset.pwType;
pwNoneHint.style.display = pwType === 'none' ? '' : 'none';
pwPinBlock.style.display = pwType === 'pin' ? '' : 'none';
pwAlphaBlock.style.display = pwType === 'alpha' ? '' : 'none';
if (pwType === 'pin') encPinDigits[0].focus();
if (pwType === 'alpha') pwAlphaInput.focus();
});
});
// ── Compression tabs ───────────────────────────────────────────────────────
document.querySelectorAll('.compress-opt').forEach(el => {
el.addEventListener('click', () => {
document.querySelectorAll('.compress-opt').forEach(o => o.classList.remove('active'));
el.classList.add('active');
compMode = el.dataset.mode;
});
});
// ── PIN keyboard handling (shared for encrypt + modal) ────────────────────
function wirePinInputs(digits) {
digits.forEach((el, i) => {
el.addEventListener('input', () => {
const v = el.value.replace(/\D/g, '').slice(-1);
el.value = v;
if (v && i < digits.length - 1) digits[i + 1].focus();
});
el.addEventListener('keydown', e => {
if (e.key === 'Backspace' && !el.value && i > 0) {
digits[i - 1].value = '';
digits[i - 1].focus();
}
if (e.key === 'ArrowLeft' && i > 0) digits[i - 1].focus();
if (e.key === 'ArrowRight' && i < digits.length - 1) digits[i + 1].focus();
});
el.addEventListener('paste', e => {
e.preventDefault();
const str = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, digits.length);
str.split('').forEach((ch, j) => { if (digits[i + j]) digits[i + j].value = ch; });
const next = Math.min(i + str.length, digits.length - 1);
digits[next].focus();
});
});
}
wirePinInputs(encPinDigits);
wirePinInputs(modalPinDigits);
// ── Show/hide password toggles ─────────────────────────────────────────────
function wireToggle(input, btn) {
btn.addEventListener('click', () => {
const show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.textContent = show ? '●' : '○';
});
}
wireToggle(pwAlphaInput, pwAlphaToggle);
wireToggle(modalPwInput, modalPwToggle);
// ── ENCRYPT ────────────────────────────────────────────────────────────────
encryptBtn.addEventListener('click', async () => {
const text = textInput.value;
if (!text.trim()) { showErr(encryptError, 'Nothing to encrypt.'); return; }
// Validate password
let password = '';
if (pwType === 'pin') {
password = encPinDigits.map(d => d.value).join('');
if (password.length !== 4) { showErr(encryptError, 'Enter all 4 PIN digits.'); return; }
} else if (pwType === 'alpha') {
password = pwAlphaInput.value;
if (!password) { showErr(encryptError, 'Enter a password or choose "None".'); return; }
}
encryptBtn.disabled = true;
encryptBtn.textContent = 'Encrypting…';
encryptError.style.display = 'none';
try {
const plainBytes = new TextEncoder().encode(text);
const origSize = plainBytes.length;
const compressed = await compress(plainBytes, compMode);
const compRatio = compMode !== 'none' ? Math.round((1 - compressed.length / origSize) * 100) : 0;
let frag, fragMode;
if (pwType === 'none') {
const parts = await encryptKey(compressed);
frag = buildFrag('k', compMode, parts);
fragMode = 'k';
} else if (pwType === 'pin') {
const parts = await encryptPassword(compressed, password);
frag = buildFrag('n', compMode, parts);
fragMode = 'n';
} else {
const parts = await encryptPassword(compressed, password);
frag = buildFrag('a', compMode, parts);
fragMode = 'a';
}
const url = `${location.origin}/share/#${frag}`;
shareLink.value = url;
resultSection.style.display = 'block';
resultBadges.innerHTML = '';
addBadge(resultBadges, '⚐ AES-256-GCM', 'green');
if (fragMode === 'k') addBadge(resultBadges, '⚿ Key in link', 'yellow');
if (fragMode === 'n') addBadge(resultBadges, '⚿ 4-digit PIN', 'red');
if (fragMode === 'a') addBadge(resultBadges, '⚿ Password locked', 'red');
if (compMode !== 'none') addBadge(resultBadges, `⊡ ${compMode} −${compRatio}%`, 'green');
resultSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
copyToClipboard(url, copyBtn);
} catch (e) {
showErr(encryptError, 'Encryption failed: ' + e.message);
} finally {
encryptBtn.disabled = false;
encryptBtn.textContent = 'Encrypt & Get Link →';
}
});
clearBtn.addEventListener('click', () => {
textInput.value = '';
pwAlphaInput.value = '';
encPinDigits.forEach(d => d.value = '');
resultSection.style.display = 'none';
encryptError.style.display = 'none';
textInput.focus();
});
copyBtn.addEventListener('click', () => copyToClipboard(shareLink.value, copyBtn));
// ── DECRYPT — init ─────────────────────────────────────────────────────────
const rawHash = location.hash.slice(1);
if (rawHash) {
parsedFrag = parseFrag(rawHash);
if (parsedFrag) {
encryptSection.style.display = 'none';
decryptSection.style.display = 'block';
if (parsedFrag.mode === 'k') {
autoDecrypt();
} else {
openModal(parsedFrag.mode);
}
}
}
// ── Modal open ─────────────────────────────────────────────────────────────
function openModal(mode) {
if (mode === 'n') {
modalLabel.textContent = '4-Digit PIN';
modalSubtitle.textContent = 'This message is PIN-protected. Enter the 4-digit PIN to decrypt it.';
modalPinRow.style.display = 'flex';
modalPwWrap.style.display = 'none';
unlockModal.classList.add('open');
setTimeout(() => modalPinDigits[0].focus(), 80);
} else {
modalLabel.textContent = 'Password';
modalSubtitle.textContent = 'This message is password-protected. Enter the password to decrypt it.';
modalPinRow.style.display = 'none';
modalPwWrap.style.display = 'flex';
unlockModal.classList.add('open');
setTimeout(() => modalPwInput.focus(), 80);
}
}
// ── Modal unlock ───────────────────────────────────────────────────────────
modalUnlockBtn.addEventListener('click', attemptUnlock);
modalPwInput.addEventListener('keydown', e => { if (e.key === 'Enter') attemptUnlock(); });
modalPinDigits[3].addEventListener('keydown', e => { if (e.key === 'Enter') attemptUnlock(); });
// auto-submit when last PIN digit filled
modalPinDigits[3].addEventListener('input', () => {
if (modalPinDigits.every(d => d.value)) attemptUnlock();
});
async function attemptUnlock() {
let password = '';
if (parsedFrag.mode === 'n') {
password = modalPinDigits.map(d => d.value).join('');
if (password.length !== 4) {
shakeModal();
modalError.textContent = 'Enter all 4 digits.';
return;
}
} else {
password = modalPwInput.value;
if (!password) {
shakeModal();
modalError.textContent = 'Enter the password.';
return;
}
}
modalUnlockBtn.disabled = true;
modalUnlockBtn.textContent = 'Decrypting…';
modalError.textContent = '';
try {
const text = await doDecrypt(parsedFrag, password);
unlockModal.classList.remove('open');
showDecrypted(text, true);
} catch {
shakeModal();
modalError.textContent = parsedFrag.mode === 'n' ? 'Wrong PIN. Try again.' : 'Wrong password. Try again.';
if (parsedFrag.mode === 'n') { modalPinDigits.forEach(d => d.value = ''); modalPinDigits[0].focus(); }
else { modalPwInput.value = ''; modalPwInput.focus(); }
} finally {
modalUnlockBtn.disabled = false;
modalUnlockBtn.textContent = 'Unlock →';
}
}
function shakeModal() {
modalBox.classList.remove('modal-shake');
void modalBox.offsetWidth; // reflow
modalBox.classList.add('modal-shake');
}
async function autoDecrypt() {
try {
const text = await doDecrypt(parsedFrag, null);
showDecrypted(text, false);
} catch {
decryptError.textContent = 'Could not decrypt — the link may be incomplete or corrupted.';
decryptError.style.display = 'block';
}
}
async function doDecrypt(frag, password) {
let plain;
if (frag.mode === 'k') {
plain = await decryptKey(frag.a, frag.b, frag.c);
} else {
plain = await decryptPassword(frag.a, frag.b, frag.c, password);
}
const decompressed = await decompress(plain, frag.comp);
return new TextDecoder().decode(decompressed);
}
function showDecrypted(text, hadPassword) {
decryptedContent.textContent = text;
decryptMeta.innerHTML = '';
addBadge(decryptMeta, '⚐ AES-256-GCM', 'green');
if (hadPassword) {
const label = parsedFrag.mode === 'n' ? '⚿ PIN verified' : '⚿ Password verified';
addBadge(decryptMeta, label, 'green');
}
if (parsedFrag.comp !== 'none') addBadge(decryptMeta, `⊡ ${parsedFrag.comp} decompressed`, 'green');
copyDecryptedBtn.addEventListener('click', () => copyToClipboard(text, copyDecryptedBtn));
}
// ── Helpers ────────────────────────────────────────────────────────────────
function addBadge(container, label, color) {
const span = document.createElement('span');
span.className = `badge badge-${color}`;
span.textContent = label;
container.appendChild(span);
}
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const orig = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = orig, 1800);
});
}
function showErr(el, msg) {
el.textContent = msg;
el.style.display = 'block';
}