docs(auth): what the third reading found, and Proton's rule (ADR 0017)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ad52da5a94
commit
1ada2e27f6
1 changed files with 103 additions and 34 deletions
|
|
@ -191,8 +191,9 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
account without a working second factor. Each is asked for a purpose
|
||||
(`EmailCodePurpose`: `signUp`, `finishSetup`, `authenticatorReset`, `accountReset`,
|
||||
`deletion`; `email_code_request.purpose`), only that purpose takes it (any other is
|
||||
refused as no longer valid and leaves it unused), and its mail says what it would do
|
||||
and to type it only in the app on that instance. Since step 3
|
||||
refused as no longer valid and leaves it unused), and its mail says what it would do,
|
||||
in its subject too (a notification shows the subject alone), and to type it only in
|
||||
the app on that instance. Since step 3
|
||||
nothing turns a code into a session (`emailCode.verifyCode`, `emailSignIn.signIn` and
|
||||
the switch token went with build 22, `minimumAppBuild` 22 in Learn).
|
||||
- **Signing up** (`passwordSignIn.signUp(request, code, setup)`, without a session): an
|
||||
|
|
@ -222,30 +223,40 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
get no more checked; an address nobody has alike), past which no proof is checked and
|
||||
nothing begins for it anywhere until the hour has passed: so whoever knows an address
|
||||
and fills its count from one network holds no other back, and guessing from many
|
||||
makes at most 30 guesses an hour. A change's proofs (a `change` challenge) count per
|
||||
account instead, 10 begun an hour, never toward the address, and no sign-in takes one.
|
||||
Every call carrying a code after the password or phrase proved (sign-in, the phrase's
|
||||
reset, a change, a new phrase, deletion) takes the account's `address` and `language`,
|
||||
refused (`wrongCredentials`) before any code is checked when it isn't theirs.
|
||||
makes at most 30 guesses an hour. A signed-in owner's proofs (a `change` challenge,
|
||||
below) never count toward the address, and no sign-in takes one. A sign-in or the
|
||||
phrase's reset carrying a code takes the account's `address` and `language`, refused
|
||||
(`wrongCredentials`) before any code is checked when it isn't theirs.
|
||||
- **The owner's proof** (`OwnerProof`, ADR 0017's third reading, as Proton has it): a
|
||||
session alone never gets a code checked. Every signed-in change of how an account is
|
||||
secured (adding an authenticator once one is confirmed, removing a confirmed one, new
|
||||
recovery codes, a new phrase, a new password, deleting) takes the password's SRP proof
|
||||
for a challenge `password.beginChange` gave (30 begun an hour per session, holding
|
||||
back no other) and then a TOTP or recovery code (`proveOwner`, `ownersCode`). A proof
|
||||
given where none is taken is refused, so none passes unchecked. Each proof counts for
|
||||
its session alone (`owner_proof_tries.dart`, `asOwnerProofTry`, table
|
||||
`session_code_failures`, kept where the session is, on a member too), before it is
|
||||
checked; the tenth wrong in a row ends the session (`sessionEnded`), only a proof
|
||||
accepted clears the count, and a try that checked none is taken back. Never toward the
|
||||
account's waits, nor waiting for them: whoever knows the password holds no signed-in
|
||||
owner out of changing it, and a new password forgets those waits.
|
||||
- **Authenticators:** a secret made on the register, shown once as an `otpauth://` URI
|
||||
(issuer: the account domain); confirmed by a code; while the account has a confirmed
|
||||
one, another counts only with a code from that one too (`wrongExistingCode`), so a
|
||||
session alone adds no second factor; the first confirmed brings ten recovery codes; at
|
||||
most 10 per account; the last confirmed one stays; removing a confirmed one, or new
|
||||
recovery codes, takes a code. Each step's code is accepted once, by a conditional
|
||||
update of `lastStep`. A signed-in session's codes here count for that session alone
|
||||
(`session_code_tries.dart`, table `session_code_failures`, kept where the session is,
|
||||
on a member too), each counted before it is checked; the tenth wrong in a row ends the
|
||||
session (`sessionEnded`), a code accepted clears the count, a try that checked no code
|
||||
is taken back; never toward the account's waits, so a stolen session holds nobody out.
|
||||
(issuer: the account domain); confirmed by a code from it; while the account has a
|
||||
confirmed one, another counts only with the owner's proof too, so a session alone adds
|
||||
no second factor; confirming one confirmed already is refused as unknown (its code is
|
||||
never checked for a session); the first confirmed brings ten recovery codes; at most
|
||||
10 per account; the last confirmed one stays; removing a confirmed one, or new recovery
|
||||
codes, takes the owner's proof, removing one never confirmed nothing. Each step's code
|
||||
is accepted once, by a conditional update of `lastStep`.
|
||||
- **Codes tried in a row** (`second_factor_waits.dart`, table
|
||||
`second_factor_failures`): every code tried against an account's second factor after
|
||||
a right password or phrase (sign-in, the phrase's reset, a change, a new phrase,
|
||||
deletion) counts, per account, from every network, before it is checked, one at a time
|
||||
a right password or phrase at sign-in or in the phrase's reset (never the owner's
|
||||
proofs while signed in) counts, per account, from every network, before it is checked, one at a time
|
||||
per account (an advisory lock), so codes sent at once get no more checked; every tenth
|
||||
in a row begins a wait as it is counted (`codesWait`, with when), an hour, then twice
|
||||
as long each time, a day at the most, and every wait mails the account's address
|
||||
(which those calls take); a code accepted, and either reset carried out, clears it.
|
||||
(which those calls take); a code accepted, either reset carried out, and a new
|
||||
password clear it.
|
||||
- **Resets:** a lost authenticator: SRP's proof plus a mailed code or the recovery
|
||||
phrase's signature asks for it; it takes effect 72 hours later, a mail tells the
|
||||
address at once, and signing in with a code calls it off; then the password alone
|
||||
|
|
@ -256,18 +267,21 @@ Built on the three parts above, on the register (members forward every step thro
|
|||
password, the keys staying. A lost password without it: a mailed code asks, 72 hours
|
||||
pass (a mail at the start, a sign-in calls it off), a new code carries it out: new
|
||||
password, new keys, no authenticators.
|
||||
- **Changing while signed in:** `password.beginChange` and `password.change` take the old
|
||||
password's SRP proof, a TOTP or recovery code and the same keys wrapped under the new
|
||||
password; `password.setRecoveryKeys` replaces the phrase's wrapping with the same
|
||||
proof and code (a phrase of one's own would otherwise set a new password through the
|
||||
phrase's reset). A session alone changes neither, nor a session and a code.
|
||||
- **Changing while signed in:** `password.change` takes the owner's proof (the old
|
||||
password's SRP proof for a `password.beginChange` challenge, and a TOTP or recovery
|
||||
code) and the same keys wrapped under the new password; `password.setRecoveryKeys`
|
||||
replaces the phrase's wrapping with the same proof (a phrase of one's own would
|
||||
otherwise set a new password through the phrase's reset). A session alone changes
|
||||
neither, nor a session and a code.
|
||||
- **Deleting an account with a password** (`account.delete(proof:)`, a `DeletionProof`)
|
||||
takes the same: the password's proof for a `beginChange` challenge, the account's
|
||||
address and a code, bound to the host the device addressed (`requireOwnerToDelete`; a
|
||||
member passes the proof on and the register checks it). Where no second factor works
|
||||
(a setup never finished, or the authenticators' reset taken effect) the code is one by
|
||||
mail asked for deleting (`request`, `code`). An account without a password deletes
|
||||
with its session, as before.
|
||||
takes the same: the password's proof for a `beginChange` challenge and a code, bound
|
||||
to the host the device addressed (`requireOwnerToDelete`; a member passes the proof on
|
||||
and the register checks it), counted for the session as every owner's proof is. The
|
||||
code is a TOTP or recovery code wherever an authenticator is confirmed; where no second
|
||||
factor works (a setup never finished, or the authenticators' reset taken effect;
|
||||
`SignInWays.secondFactorWorks` tells the app) it may be one by mail asked for deleting
|
||||
(`request`, `code`), to the account's own address. An account without a password
|
||||
deletes with its session, as before.
|
||||
- **Sessions end everywhere:** a change of password ends every other session of the
|
||||
account; a reset of the password or the account ends all. The register ends its own
|
||||
and keeps a `sessions_ended_notice` per member (told at once and hourly until it
|
||||
|
|
|
|||
Loading…
Reference in a new issue