PRIVACY

TEXT ENCRYPT

Encrypt any text into a shareable link. Password-lockable. AES-256, browser-only — nothing stored anywhere.

Password lock
None
4-digit PIN
Alphanumeric
No password — the decryption key is embedded in the link.
Compression
None
Fastest
Deflate
Shorter URL
Gzip
Max compact
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';
}