mysecret.now

Send secrets securely

How mysecret.now works

mysecret.now is a zero-knowledge secret sharing utility. Everything that protects your secret - the encryption key, the plaintext, the decryption - happens in the browser. The server is little more than an untrusted mailbox for opaque ciphertext.

The encryption flow

  1. You paste a password or any sensitive text on the home page.
  2. The browser generates a four-word English passphrase from the EFF diceware wordlist and derives a 256-bit AES-GCM key from it via Argon2id (64 MiB memory, 3 iterations, 4 parallel lanes, per-secret random salt). Argon2id is memory-hard and OWASP-recommended, so an attacker cannot cheaply parallelize brute-force attempts on GPUs or ASICs.
  3. A random 96-bit initialization vector (IV) is generated with crypto.getRandomValues.
  4. The browser encrypts your plaintext with crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, data).
  5. Only ciphertext, iv, the non-secret salt, and your chosen expiry are POST'ed to the server. The passphrase is never sent.
  6. The server stores the ciphertext, IV, and salt in PostgreSQL and returns a payload ID.

The decryption flow

  1. The recipient opens the link, e.g. https://mysecret.now/v/01HZ….
  2. The server does an atomic DELETE … RETURNING on the row, returning the ciphertext, IV, and salt. The record is deleted before the response is sent - so even two concurrent readers cannot both receive the payload.
  3. The recipient enters the four-word passphrase (received over a separate channel).
  4. The browser derives the AES-256 key from the passphrase + salt via Argon2id and calls decrypt. The plaintext appears in the browser memory only.

Expiry and cleanup

Every row has an expires_at timestamp. The read path refuses to return expired rows. A background node-cron job inside the API process runs every 15 minutes and hard-deletes all rows where expires_at <= now().

Why a four-word passphrase

The decryption key is a four-word English passphrase drawn from the 7,776-word EFF diceware wordlist - roughly 3.6 quadrillion combinations (77764). Each guess has to run an Argon2id memory-hard derivation (64 MiB of memory, 3 passes, 4 parallel lanes) before it can be tested against the ciphertext - meaning an attacker cannot simply throw thousands of GPUs at it the way they can with PBKDF2 or bcrypt. Despite a nominal ~51.8 bits of entropy, password strength checkers such as Bitwarden’s typically rate this kind of diceware passphrase at centuries to crack offline, and Argon2id’s memory cost raises that further on commodity hardware. It is also far easier to read, dictate over the phone, and recognize than a random-character string.

Proof of Work

To make brute-force creation of secrets expensive, the server issues an HMAC-signed PoW challenge. The client finds a nonce such that SHA-256(seed + nonce) has a configurable number of leading zero bits. The server verifies both the HMAC and the hash before accepting the write.