| 1 | import { devAuth } from "./dev.js"; |
| 2 | import type { Env } from "../env.js"; |
| 3 | import { hmacSha256, matchesAnyKey, signingSecrets } from "../lib/crypto.js"; |
| 4 | import { b64url } from "../lib/util.js"; |
| 5 | |
| 6 | /** |
| 7 | * Proof that someone can read the invited address's email. The invite link shown to the |
| 8 | * person who sent it creates nothing on its own: only the copy emailed to the invited |
| 9 | * address carries this proof, so nobody can join as an address they don't own (and so |
| 10 | * collect its other invitations, or a bootstrap administrator's rights). |
| 11 | * The key is derived from MASTER_KEY, so emailed links stay valid across deploys, and across a |
| 12 | * rotation of MASTER_KEY while the old one is kept as MASTER_KEY_PREV. |
| 13 | */ |
| 14 | function secrets(env: Env): string[] { |
| 15 | // Local development has no MASTER_KEY unless one is passed; production always does. |
| 16 | return signingSecrets(env, devAuth(env) ? "diffinery-dev-invite-proof" : null, "MASTER_KEY is not set, so invitation emails can't be signed"); |
| 17 | } |
| 18 | |
| 19 | const proofWith = async (secret: string, tokenHash: string): Promise<string> => b64url(await hmacSha256(`diffinery-invite-proof-v1:${secret}`, tokenHash)); |
| 20 | |
| 21 | /** The proof for the invite whose token hashes to `tokenHash`. */ |
| 22 | export async function inviteProof(env: Env, tokenHash: string): Promise<string> { |
| 23 | return proofWith(secrets(env)[0]!, tokenHash); |
| 24 | } |
| 25 | |
| 26 | /** Whether `proof` is the invite's proof, made with MASTER_KEY or, while it is being rotated, MASTER_KEY_PREV. */ |
| 27 | export async function validInviteProof(env: Env, tokenHash: string, proof: string | undefined | null): Promise<boolean> { |
| 28 | if (!proof || proof.length > 100) return false; |
| 29 | return matchesAnyKey(secrets(env), proof, (s) => proofWith(s, tokenHash)); |
| 30 | } |