Skip to content

Bring the privacy policy, terms and site copy up to date with end-to-end encryption - #15

Merged
cdeutsch merged 33 commits into
mainfrom
claude/e2e-encryption-legal-review-ceb415
Oct 27, 2026
Merged

cdeutsch merged 33 commits into
mainfrom
claude/e2e-encryption-legal-review-ceb415

Conversation

@cdeutsch

Copy link
Copy Markdown
Contributor

The privacy policy and terms still described the app from before end-to-end encryption, and several pages promised more about the password than the code delivers. This brings them in line with what ships, and removes the Chrome extension's Axiom logging, which sent user and push ids to a third party the policy never named.

What changed

Axiom removed. The extension's debug and logError now write to the service worker's console only. @axiomhq/js, VITE_AXIOM_TOKEN and .env.example are gone, along with their mentions in the docs, CI and AGENTS.md.

Privacy policy.

  • Opens with what is encrypted and links to /security.
  • Defines "Account and Push Information": what is still readable, including the key wraps and, for older accounts, the deviceNames array.
  • Names Google Analytics for Firebase, which the Android and iOS apps send to.
  • Says a lawful request can only get ciphertext.
  • Covers retention: links kept until deleted, files kept for the period of the plan they were pushed on, locked pushes after a reset without the recovery key, and pushes from before encryption.
  • Gains a "last updated" date.

Terms.

  • New section on encryption and the recovery key: the user holds both the password and the recovery key, and pushes lost with both cannot be recovered.
  • The "pre-screen" and "disclose content" clauses now say content is not reviewed and can only be handed over encrypted.
  • The file-retention sentence now matches billing.
  • New effective date.

Where the password goes. The legal pages, the Security page, the front page and the recovery-key screen in all four apps used to say the password is never sent. It does reach Firebase Authentication in three cases: a reset on Firebase's hosted form, a sign-in whose derived key is refused (a typo, or the first sign-in after a reset), and an app from before encryption. The pages now say so, or no longer make the claim.

Why the fallback stays. docs/e2ee.md and a comment at each client's raw-password fallback now record that every password reset depends on it, so it must not be removed once old accounts have moved.

Smaller corrections.

  • The password unlocks the key pushes are encrypted with; the pages no longer say it is that key.
  • The one-step deletion of locked pushes is no longer promised: only /reset offers it, and no reset reaches that page today.

PR 13's front-page claims are reconciled with the same wording.

For the reviewer

  • Merging deploys apps/web. If this merges after 2026-10-27, update the dates in Privacy.tsx ("Last updated") and Terms.tsx ("effective as of") first.

  • Outside the code:

    • revoke the Axiom ingest token and delete the push-that-chrome dataset, since installed extension builds keep sending until they update;
    • decide whether to clear the old deviceNames arrays from user documents. The policy discloses them for now.
  • Firebase Analytics is disclosed, not disabled. Turning it off is a separate change, and on Android one that needs a release note.

  • Checks run:

    • apps/web: lint, test:keys and both builds pass;
    • Android: unit tests and assembleDebug pass;
    • iOS: builds;
    • macOS: fails only on the deployment-target errors docs/macos.md describes.

    The Android string change is on a screen shown once, at key setup, so whatsnew is unchanged.

🤖 Generated with Claude Code

cdeutsch and others added 30 commits October 27, 2026 14:26
The extension's service worker sent its debug and error logs to Axiom whenever it was built with an ingest token. Those logs carried user ids, push ids and error strings, and Axiom was a third party the privacy policy never named. With pushes now end-to-end encrypted, the extension should not be passing identifiers to anyone else, so the logging is removed rather than disclosed.

`debug` and `logError` now write to the service worker's console only. The `@axiomhq/js` dependency, `VITE_AXIOM_TOKEN` and the `.env.example` that existed only for it are gone, along with the mentions in the docs, CI and AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The policy still described the app from before end-to-end encryption: it said the links and files people push "are stored in a database" and that Firebase stores them, without saying that both now hold only ciphertext. It also never listed the metadata that is still readable, since its definition of personal information covered only device and subscription information.

The policy now opens with what is encrypted and links to the Security page. It defines "Account and Push Information" (email, device tokens, the random device id, each push's account, time, kind, sending device, encrypted content and blind search tokens, the rough file size and the monthly counts) and says what it is used for. It also says that a lawful request can only be answered with ciphertext, and how long pushes are kept, including what a reset without the recovery key and pushes from before encryption mean for that. It gains a "last updated" date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ost password

The terms still reserved rights the encrypted app cannot exercise: to "pre-screen" content, and to access and disclose it as if it were readable. They also said nothing about the one loss the design makes certain: a password reset without the recovery key leaves the earlier pushes unreadable by anyone.

A new section under the account and password terms says what is encrypted, that the user alone holds the password and the recovery key, and that pushes lost with both are not recoverable or something we are liable for. The pre-screening clause keeps the right to refuse and delete content but says it is not reviewed, and the disclosure clause says content can only be handed over encrypted. The effective date moves to 10/27/2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy policy, the terms and the Security page all said the password is never sent. Two paths send it to Firebase Auth. A reset runs on Firebase's hosted form, which takes the new password as typed. And `signInEitherWay` retries with the raw password whenever `authKey` is refused, so that accounts made before encryption can still sign in; that covers every mistyped password and the first sign-in after a reset. The policy also said nothing could be worked out from the derived key, when a weak password can be found from it by guessing, as the Security page itself says.

The three pages now say what is true: the recovery key is never sent, the app signs in with the derived key, the raw password reaches Firebase Authentication on a reset and on the fallback, and the next sign-in after a reset replaces it with the derived key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit inserted the sentences about when the password reaches Google between "That second key never leaves the device" and "It also means we cannot reset your password for you", so "It" read as referring to the reset. The reset sentences now come after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t is unreadable

The privacy policy and the terms said Push Content is held, and can be disclosed, only in encrypted form, and that it cannot be decrypted or reviewed. That is not true of pushes made before encryption: a link stays as it was sent until an updated app signs in to the account, which for an account that never updates is indefinitely, and a file keeps its plaintext bytes and name until it expires. The privacy policy's retention section already said so, so the documents contradicted themselves.

Each of those statements now carries the exception. The terms' pre-screening clause drops "cannot" and keeps "does not", which is true either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android and iOS apps link Firebase Analytics and never turn its collection off, so they send its automatic usage events, with an identifier for the installation, to Google. The policy's sharing section described Google only as the App's storage and notification service. Axiom was just removed for being a third party the policy did not name, and this left the same gap open.

The sharing section now names it and says no Push Content is in those events: neither app logs an event of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ushed on

The terms said a file is kept for "the file retention period of the plan the account is then on", which reads as the plan at the time of deletion. Retention is fixed when the file is pushed and nothing moves it afterwards (docs/billing.md), which is what the privacy policy now says, so the two documents disagreed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it makes it

Both legal pages said Push Content is encrypted "with a key made from your password". Pushes are encrypted with a random account key; the key made from the password only wraps it, and the recovery key wraps it a second time (docs/e2ee.md, Keys). The old wording implied that a password change re-encrypts every push and that the recovery key is a different encryption key. They now say the key is one only the password or the recovery key can unlock, as the Security page does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery key

The terms said the recovery key is shown "when you register". An account made before encryption is shown it at its first sign-in with an updated app, when its keys are made (`openKeys` in `signIn.ts`), so for every existing account the sentence described something that never happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy policy said a link push is kept until it is deleted "or your account is closed", which suggests a way to close an account that deletes its pushes. There is none: no app offers it and no function deletes an account's pushes, so closing one is a request to us, as the terms' termination section and the subscription sentence above already say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list of what is kept to deliver pushes left out `e2ee.wraps` on the user document: the account key encrypted under the password's key and under the recovery key. It is what lets someone with the database try guesses at a weak password offline, so a list that reads as complete should name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`pushMessageOf` sends `keyVersion` with the push's id, type and sending device, and the policy's sentence read as a complete list of the three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The terms defined "Push Content" as the links and files pushed and their titles and names, while the privacy policy also counts a file's type. A defined term used across both documents should mean one thing, so the terms now point at the policy's definition and repeat it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`debug` and `logError` were async so that callers could wait for the Axiom ingest. With that gone they only write to the console, and the `async` and the `await` at each call suggested I/O that no longer happens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts recovery key

The page said the recovery key is shown "when you create your account". An account made before encryption is shown it the first time an updated app signs in, when its keys are made. The terms were corrected for the same thing and now point readers here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy policy and the Security page said a refused sign-in retries with the password "as typed". Android's `signInToFirebase` also tries it trimmed, since older Android builds trimmed it at registration, so "as typed" was not always the password that was sent. "The password itself" is true on every client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Security page ended its reset sentence with "replaces it with the key", in a paragraph that has just introduced two keys, so neither "it" nor "the key" was clear. It now names the password Google holds and the first key, the one that signs in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The screen said of the password "We never see it and cannot reset it for you". It is true that we never read it, but the raw password does reach Firebase Authentication on a reset and on the sign-in fallback, as the privacy policy now says, and the screen is not the place to argue that difference. What the user needs from it is that the password is the key and cannot be reset without losing the pushes.

The sentence is now "Your password is the key to your pushes, and we cannot reset it for you." in docs/e2ee.md and in the web, Android, iOS and macOS apps, which copy it word for word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comments on the raw-password fallback said it was for accounts made before encryption, which reads as if it can be removed once they have all moved. It cannot: a reset runs on Firebase's hosted form, which sets the raw new password as the Firebase password, so the first sign-in after every reset gets in through the fallback and then moves the account back to `authKey`. Removing it was suggested on exactly that reading, and would have left every user who resets unable to sign in.

The web, Android, iOS and macOS sign-in now say so beside the fallback, and when it can go: once a reset sets `authKey` itself, which needs the reset email's action URL unlocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/e2ee.md said the clients sign in with `authKey` and "never the password itself", and described the fallback only under Migration, as a path for accounts made before encryption. That reading is how removing it was suggested. Every reset runs on Firebase's hosted form, which sets the raw new password, so the first sign-in after any reset depends on the fallback. The comments added at each client's fallback cite the doc's Password reset section for this, and it did not say it.

The Keys section now names the fallback. The Password reset section says the next sign-in gets in through it, that `updatePassword(authKey)` follows once the recovery step is done, that the fallback can go only when `/reset` sets `authKey` itself, and that this is why the raw password reaches Firebase Authentication on a reset and on a refused sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy policy and the Security page said that after a reset without the recovery key, the app offers to delete the old pushes in one step. Only the `/reset` page makes that offer, and no reset reaches it while the email template's action URL is locked. Every reset goes through Firebase's hosted form and then the sign-in recovery prompt, whose "Start over" on web, Android, iOS and macOS makes new keys with no deletion offer.

Both pages now say the old pushes stay as locked rows that can be deleted. The design doc's Password reset section says which page makes the offer and that it is not reached today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Google

The privacy policy and the Security page listed two cases in which the raw password reaches Firebase Authentication, and the Security page said "only". A version of an app from before encryption, which the Security page covers under old versions, sends the raw password on every sign-in; Firebase refuses it once the account has moved, but it has still been sent. Both pages now name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…done

Both pages said the next sign-in after a reset replaces the raw password with the derived key. `updatePassword(authKey)` runs only in `finish`, after the recovery key is entered or the user starts over, on every client. Someone who signs out at the recovery prompt keeps the raw password in Firebase, and the next sign-in sends it again. The pages now say when the replacement happens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before encryption, the push trigger kept a `deviceNames` array of device model names on the user document. The trigger stopped writing it, but nothing removes it from documents that already have one, so an account made before encryption may still hold it. The policy's list of Account and Push Information now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sentences added for encryption wrote "account's" and "plan's" with straight apostrophes beside "Google’s" and "Apple’s" with curly ones in the same paragraphs. The terms use straight apostrophes throughout and keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was there for `import.meta.env.VITE_AXIOM_TOKEN`, and nothing in the worker reads `import.meta` since the Axiom logging was removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The privacy policy and the Security page called the device name that pushes used to carry a model name. Before encryption, Android sent the name the owner gave the phone (`device_name`, falling back to `Build.MODEL`), and the web app sent `navigator.userAgent`. A name someone gave their phone often has their own name in it, so the pages described that data as narrower than it was. Both now say what it could be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both pages announced "two cases" and then named a third, an app from before encryption. Without a count, the list cannot go stale if it changes again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cdeutsch and others added 3 commits October 27, 2026 15:38
The front page's "Sign in everywhere" card said the password "never leaves" the devices. It reaches Firebase Authentication on a reset, on a sign-in whose derived key is refused and from an app from before encryption, as the privacy policy and the Security page now say. The card now says only that the password unlocks the key on each device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ADME

The front page said the app "turns your password into the key", and the README that pushes are encrypted "with a key made from the account password". Pushes are encrypted with a random account key that the password's key only unlocks, and the recovery key unlocks a second time (docs/e2ee.md, Keys). That is how a password change avoids re-encrypting every push. The legal pages and the Security page already say it this way, and these two now match them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit lengthened one line of it past the width the rest of the file keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added web The web app and the Chrome extension (apps/web) android The Android app (apps/android) ios The iOS app (apps/ios) macos The macOS app (apps/macos) labels Oct 27, 2026
@cdeutsch
cdeutsch merged commit 874e877 into main Oct 27, 2026
10 checks passed
@cdeutsch
cdeutsch deleted the claude/e2e-encryption-legal-review-ceb415 branch October 27, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

android The Android app (apps/android) ios The iOS app (apps/ios) macos The macOS app (apps/macos) web The web app and the Chrome extension (apps/web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant