Skip to content

feat(share): add optional password protection for document links #300

Description

@MerciHanrim

Problem

A share link carries the whole document in the URL fragment: nodes, edges, labels, saved frames and any imported spreadsheet rows. Anyone who gets the link text can open it. The link text does not stay with the recipient only: it remains in the sender's and the recipient's browser history with the full fragment even though the app clears the address bar, and it sits in the clipboard and in whatever chat or mail carried it.

There is no way today to send a link that is useless to someone who finds it later.

Proposal

Share offers two choices:

  • Plain link stays the default and behaves as today.
  • Protect with a password encrypts the document before it goes into the link.

This is a separate issue from #297 and depends on it and on #296 (see Dependencies). Implementation needs its own approval; this issue records the contract.

Contract

Creating

  • The plain link is the default. Choosing protection is an explicit action.
  • A protected link holds only the ciphertext, a random salt and a random IV, under a fixed format prefix. The password, the key and the iteration count are never in the link.
  • Loop Studio does not store or transmit the password or the key. The UI does not say "not stored anywhere": a browser or a password manager may offer to save a password, and the product does not hide that.
  • The dialog tells the sender to send the password through a different channel than the link.
  • The dialog says a lost password cannot be recovered: there is no server and no reset.
  • The plain link shows a notice that the document data is in the URL and stays in browser history.
  • The outbound size cap applies to the protected payload exactly as it does to a plain one: over the cap is a hard reject, never a truncation.

Opening

  • The recipient must enter the password before anything from the shared document is drawn. Nothing derived from the ciphertext (a title, a node count) is shown before that.
  • A wrong password, a failed authentication and a damaged ciphertext produce exactly the same error and exactly the same console output.
  • A failed or cancelled open leaves the current document and any run in progress untouched, as today.
  • After a successful open the existing rules apply: replace confirmation unless the session is the untouched sample, one load, no auto-run.
  • Whether the decrypted document is kept in browser storage follows feat(storage): add temporary sessions and shared-browser privacy controls #297: a personal session stores it by the existing policy; a temporary session never persists the decrypted plaintext.

What it does not claim

  • It cannot hide the length of the ciphertext or the fact that a link was used.
  • It is only as strong as the password. The ciphertext, salt and IV stay in browser history and chat logs, so guesses can be tried offline with no limit on attempts. The UI says so.
  • It is not sign-in and not access control. Anyone with the link and the password can open, edit and re-share the document as a plain link.

Decisions

  1. Prefix and spec. A protected link is #p1=… under a new spec, loop-share-protected/1. Builds that predate it treat p1= as a foreign fragment and leave it in the address bar, so the link can be opened again after the app updates. loop-share/1 stays frozen; the data it really carries is recorded as an erratum (see Implementation gates).
  2. Method. AES-256-GCM with a 16-byte random salt, a 12-byte random IV and a 128-bit authentication tag. The key is derived with PBKDF2-HMAC-SHA-256 at 600,000 iterations. The iteration count is fixed by the p1 format and never read from the link. Web Crypto only; no cryptographic code is written or bundled here. The salt and the IV are always generated with crypto.getRandomValues. The AES key derived by PBKDF2 is created as a non-extractable CryptoKey and is never exported. References: W3C Web Cryptography, OWASP Password Storage.
  3. Password rules. 12 to 128 characters. Input is normalised to NFC. Leading and trailing whitespace is not trimmed; it is part of the password. Creating a link has a confirmation field and a show / hide control. Pasting and password managers are not blocked. The dialog suggests a long sentence or several words.
  4. Removing the fragment. As soon as the format is confirmed to be p1 and the ciphertext has been copied into memory, the fragment is removed from the address bar. During cancel, failure and retry the ciphertext is held in memory only. After a reload the original link has to be opened again.
  5. A broken structure. What can be checked before any cryptographic operation (minimum length for salt, IV and tag) is checked first. A link that is plainly broken shows the general error without asking for a password. A wrong password, a failed authentication and a damaged ciphertext are indistinguishable.
  6. Retries. No fixed limit and no lock-out. One operation runs at a time and the button is disabled while it runs. The wording states that offline guessing cannot be prevented.
  7. No Web Crypto. When creating, the protection choice is disabled and the reason is shown. When opening a protected link, the app says a supported current browser and HTTPS are required, and in this case the fragment is not removed, so the link can be opened again elsewhere.
  8. Password managers. The contract is the sentence in bold above, not "not stored anywhere". The input attributes are set for the creating and the opening purpose and verified in real browsers; the HTML standard offers autofill hints but does not promise control over a save offer. Reference: WHATWG HTML.
  9. Order relative to feat(storage): add temporary sessions and shared-browser privacy controls #297. Protected sharing ships only after the feat(storage): add temporary sessions and shared-browser privacy controls #297 main implementation.
  10. Opening order. Fixed, below.
  11. Authenticated data. The fixed format identifier of loop-share-protected/1 and the payload header are bound as AES-GCM additional authenticated data. Re-labelling a link as another version or algorithm fails authentication. The salt and IV are in the link; the password, the key and the iteration count are not.
  12. Desktop and mobile. Encryption and link assembly are not implemented once per surface. One shared encode / decode module and one shared dialog contract; desktop and mobile differ only in the entry point.

Opening order (fixed)

  1. the feat(storage): add temporary sessions and shared-browser privacy controls #297 gate
  2. p1 structure check
  3. ciphertext copied into memory, fragment removed from the address bar
  4. password prompt
  5. key derivation
  6. decrypt and authenticate
  7. bounded inflate
  8. parse and validate
  9. replace confirmation
  10. exactly one load
  11. the guided tour, after that

Creating runs the other way: serialize, compress, encrypt, base64url. Compression comes before encryption because ciphertext does not compress, and the existing decompression-bomb cap still applies after decryption.

What was measured (read-only, main 3f07c11, Chrome 154, throwaway profiles)

Question Finding
Web Crypto on a secure origin and on the portable file:// build available on both; AES-GCM round trip succeeds
Size added by salt, IV and tag 44 bytes, about 59 characters of payload
Largest bundled template as a plain link 6,322 of the 8,192-character cap; about 6,381 protected
PBKDF2-HMAC-SHA-256, 600,000 iterations, one desktop 80 to 88 ms
Wrong password, flipped bit, truncated ciphertext, wrong IV the same OperationError with no message
Data shorter than the tag OperationError with a different message, so the message must never be shown or logged
Today's build opening a g2= fragment the fragment is stripped, a console warning only, nothing visible
Today's build opening a fragment with an unknown non-g prefix left in the address bar untouched

Dependencies

Implementation gates

  • A phone measurement of PBKDF2-HMAC-SHA-256 at 600,000 iterations, before implementation starts.
  • The password inputs verified in real browsers for what a browser or password manager offers to save.
  • An erratum for loop-share/1 that lists what a plain link really carries: saved frames and imported data are in it today, and the frozen text names only nodes, edges and the run config. The frozen document itself is not rewritten.
  • Source text in Korean and English is written first; the other sixteen locales follow, and a missing translation fails the build.

Verification

  • Round trip in each build flavour (web, PWA, portable), and a link made in one opened in the others.
  • Wrong password, one flipped character, a truncated link, a link with a swapped salt or IV: one identical error, identical console output, the current document and run untouched.
  • A link too short to hold salt, IV and tag: the general error, with no password prompt.
  • A link re-labelled with another format identifier fails authentication.
  • The protected payload obeys the outbound cap and the bounded inflate.
  • Nothing from the shared document is in the DOM before a correct password: checked in the accessibility tree, not by screenshot.
  • The password and the key are absent from browser storage, from the link, and from the history entry. The ciphertext, salt and IV do remain in the history entry, and the test states that rather than hiding it.
  • In a temporary session (feat(storage): add temporary sessions and shared-browser privacy controls #297) the decrypted plaintext is not persisted.
  • Without Web Crypto: the choice is disabled with its reason when creating; opening shows the requirement and leaves the fragment in place.
  • An older build leaves #p1=… in the address bar, and the same link opens after the update.
  • A plain link made by this build still opens in the previous build, and a plain link from the previous build opens here.
  • Keyboard and screen-reader path through both dialogs, forced colours, right-to-left.

Out of scope

  • Any server: accounts, password reset, link expiry, revocation.
  • Encrypting what is kept in browser storage.
  • Hiding the ciphertext length or that a link exists.

Not measured

  • Browsers other than Chrome 154.
  • KDF time on a phone (an implementation gate above).
  • Whether a browser offers to save the password.
  • Behaviour on a non-secure origin; the absence of Web Crypto there is taken from the standard, not from a measurement.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions