halkyon/packages/halkyon_base_server/migrations/20261020111123780-passwords/migration.sql
Tizian 「ティツィアーン」 a3a8ed3890
feat(auth): a password with TOTP on the register, and resets
The register's side of signing in with an address, a password that never
leaves the device (SRP-6a) and a code from an authenticator (ADR 0017),
with members forwarding every step through AccountRegister:

- password.set: salt, Argon2id's costs, SRP's verifier, the public keys
  and the private keys wrapped twice, all made on the device; with the
  address a mailed code proves, an anonymous person becomes identified
  under their ID. It signs in once an authenticator is confirmed.
- passwordSignIn.begin and complete: SRP's two steps with a TOTP or
  recovery code. An address nobody has is answered from a decoy key as
  an account would be; every wrong credential is refused alike.
- authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound
  to their row), each code accepted once, ten recovery codes with the
  first, the last one kept.
- accountRecovery: a lost authenticator reset 72 hours after the
  password and a mailed code or the recovery phrase ask, a lost password
  set anew with the phrase's signature, or the account reset with new
  keys 72 hours after a mailed code asks; a mail at each start, a
  sign-in calling it off.
- An account with a password no longer signs in by a mailed code or
  GitHub; no new GitHub account is linked where passwords are offered,
  and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go.

Five tables, added by the module's migration; deletePerson deletes
their rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-22 18:11:10 +02:00

148 lines
4.1 KiB
PL/PgSQL

BEGIN;
--
-- Function: gen_random_uuid_v7()
-- Source: https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
-- License: MIT (copyright notice included on the generator source code).
--
create or replace function gen_random_uuid_v7()
returns uuid
as $$
begin
-- use random v4 uuid as starting point (which has the same variant we need)
-- then overlay timestamp
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
return encode(
set_bit(
set_bit(
overlay(uuid_send(gen_random_uuid())
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
from 1 for 6
),
52, 1
),
53, 1
),
'hex')::uuid;
end
$$
language plpgsql
volatile;
--
-- ACTION CREATE TABLE
--
CREATE TABLE "account_password" (
"id" bigserial PRIMARY KEY,
"authUserId" uuid NOT NULL,
"salt" bytea NOT NULL,
"kdfAlgorithm" text NOT NULL,
"kdfOpsLimit" bigint NOT NULL,
"kdfMemLimit" bigint NOT NULL,
"verifier" bytea NOT NULL,
"encryptionPublicKey" bytea NOT NULL,
"signingPublicKey" bytea NOT NULL,
"passwordKeysNonce" bytea NOT NULL,
"passwordKeysCipherText" bytea NOT NULL,
"recoveryKeysNonce" bytea NOT NULL,
"recoveryKeysCipherText" bytea NOT NULL,
"createdAt" timestamp without time zone NOT NULL,
"updatedAt" timestamp without time zone NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "account_password_person_idx" ON "account_password" USING btree ("authUserId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "account_reset" (
"id" bigserial PRIMARY KEY,
"authUserId" uuid NOT NULL,
"kind" text NOT NULL,
"takesEffectAt" timestamp without time zone NOT NULL,
"createdAt" timestamp without time zone NOT NULL
);
-- Indexes
CREATE UNIQUE INDEX "account_reset_person_kind_idx" ON "account_reset" USING btree ("authUserId", "kind");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "recovery_code" (
"id" bigserial PRIMARY KEY,
"authUserId" uuid NOT NULL,
"hash" text NOT NULL
);
-- Indexes
CREATE INDEX "recovery_code_person_idx" ON "recovery_code" USING btree ("authUserId");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "sign_in_challenge" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"authUserId" uuid,
"purpose" text NOT NULL,
"addressHash" text NOT NULL,
"secret" text NOT NULL,
"serverValue" text,
"createdAt" timestamp without time zone NOT NULL
);
-- Indexes
CREATE INDEX "sign_in_challenge_created_idx" ON "sign_in_challenge" USING btree ("createdAt");
--
-- ACTION CREATE TABLE
--
CREATE TABLE "totp_authenticator" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid_v7(),
"authUserId" uuid NOT NULL,
"name" text NOT NULL,
"secret" bytea NOT NULL,
"confirmedAt" timestamp without time zone,
"lastStep" bigint,
"createdAt" timestamp without time zone NOT NULL
);
-- Indexes
CREATE INDEX "totp_authenticator_person_idx" ON "totp_authenticator" USING btree ("authUserId");
--
-- MIGRATION VERSION FOR halkyon_base
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('halkyon_base', '20261020111123780-passwords', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20261020111123780-passwords', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod', '20260824182259319', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_idp
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_idp', '20260924105404509', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
--
-- MIGRATION VERSION FOR serverpod_auth_core
--
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
VALUES ('serverpod_auth_core', '20260924105232991', now())
ON CONFLICT ("module")
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
COMMIT;