You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(share): add optional password protection for document links #300
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.
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
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
ciphertext copied into memory, fragment removed from the address bar
password prompt
key derivation
decrypt and authenticate
bounded inflate
parse and validate
replace confirmation
exactly one load
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
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.
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
Shareoffers two choices: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
Opening
What it does not claim
Decisions
#p1=…under a new spec,loop-share-protected/1. Builds that predate it treatp1=as a foreign fragment and leave it in the address bar, so the link can be opened again after the app updates.loop-share/1stays frozen; the data it really carries is recorded as an erratum (see Implementation gates).p1format 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 withcrypto.getRandomValues. The AES key derived by PBKDF2 is created as a non-extractableCryptoKeyand is never exported. References: W3C Web Cryptography, OWASP Password Storage.p1and 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.loop-share-protected/1and 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.Opening order (fixed)
p1structure checkCreating 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)file://buildOperationErrorwith no messageOperationErrorwith a different message, so the message must never be shown or loggedg2=fragmentgprefixDependencies
Implementation gates
loop-share/1that 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.Verification
#p1=…in the address bar, and the same link opens after the update.Out of scope
Not measured