tenseleyflow/shithub / cf0e239

Browse files

Add migrations 0009-0011: user_totp (encrypted-at-rest), user_recovery_codes, auth_audit_log

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
cf0e2399bf3a3ea7fd6e23788ec0a83fcfa4a8bc
Parents
a7e8432
Tree
65efee8

3 changed files

StatusFile+-
A internal/migrationsfs/migrations/0009_user_totp.sql 30 0
A internal/migrationsfs/migrations/0010_user_recovery_codes.sql 21 0
A internal/migrationsfs/migrations/0011_auth_audit_log.sql 31 0
internal/migrationsfs/migrations/0009_user_totp.sqladded
@@ -0,0 +1,30 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- TOTP (time-based one-time password) secrets, one per user. The secret
4
+-- is encrypted at rest with chacha20poly1305 (key from config: secrets.totp_key).
5
+-- secret_nonce is the per-row 12-byte AEAD nonce; never reused for the same key.
6
+--
7
+-- last_used_counter is the highest TOTP step counter we've accepted; codes
8
+-- whose counter is <= this value are rejected to prevent replay (RFC 6238 §5.2).
9
+-- confirmed_at is NULL during enrollment until the user proves possession of
10
+-- the authenticator with a fresh code.
11
+
12
+-- +goose Up
13
+CREATE TABLE user_totp (
14
+    id                bigserial   PRIMARY KEY,
15
+    user_id           bigint      NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
16
+    secret_encrypted  bytea       NOT NULL,
17
+    secret_nonce      bytea       NOT NULL,
18
+    confirmed_at      timestamptz,
19
+    last_used_counter bigint      NOT NULL DEFAULT 0,
20
+    created_at        timestamptz NOT NULL DEFAULT now(),
21
+    updated_at        timestamptz NOT NULL DEFAULT now(),
22
+
23
+    CONSTRAINT user_totp_nonce_size CHECK (octet_length(secret_nonce) = 12)
24
+);
25
+
26
+CREATE TRIGGER set_updated_at BEFORE UPDATE ON user_totp
27
+    FOR EACH ROW EXECUTE FUNCTION tg_set_updated_at();
28
+
29
+-- +goose Down
30
+DROP TABLE IF EXISTS user_totp;
internal/migrationsfs/migrations/0010_user_recovery_codes.sqladded
@@ -0,0 +1,21 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- Recovery codes. Generated when the user enrolls TOTP, regenerated on
4
+-- demand. Stored as sha256 hashes — the plaintext is shown to the user
5
+-- exactly once at generation time. Single-use via used_at.
6
+
7
+-- +goose Up
8
+CREATE TABLE user_recovery_codes (
9
+    id           bigserial   PRIMARY KEY,
10
+    user_id      bigint      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
11
+    code_hash    bytea       NOT NULL,
12
+    used_at      timestamptz,
13
+    generated_at timestamptz NOT NULL DEFAULT now(),
14
+    created_at   timestamptz NOT NULL DEFAULT now()
15
+);
16
+
17
+CREATE INDEX user_recovery_codes_user_id_idx ON user_recovery_codes (user_id);
18
+CREATE UNIQUE INDEX user_recovery_codes_hash_uidx ON user_recovery_codes (code_hash);
19
+
20
+-- +goose Down
21
+DROP TABLE IF EXISTS user_recovery_codes;
internal/migrationsfs/migrations/0011_auth_audit_log.sqladded
@@ -0,0 +1,31 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- Generic audit log for security-relevant events. Schema is intentionally
4
+-- broad so future sprints can reuse it (S15 permissions, S30 org changes,
5
+-- S34 admin actions, S07 SSH key changes).
6
+--
7
+-- - actor_id: the user performing the action (NULL for unauthenticated /
8
+--   admin-CLI actions; admin actions populate meta->>'admin' instead).
9
+-- - action: short snake_case verb (e.g. '2fa_enabled', 'recovery_regenerated',
10
+--   'admin_cleared_2fa').
11
+-- - target_type, target_id: the entity affected (e.g. 'user', user.id).
12
+-- - meta: free-form JSON for action-specific detail (no secrets here).
13
+
14
+-- +goose Up
15
+CREATE TABLE auth_audit_log (
16
+    id          bigserial   PRIMARY KEY,
17
+    actor_id    bigint      REFERENCES users(id) ON DELETE SET NULL,
18
+    action      text        NOT NULL,
19
+    target_type text        NOT NULL,
20
+    target_id   bigint,
21
+    meta        jsonb       NOT NULL DEFAULT '{}'::jsonb,
22
+    created_at  timestamptz NOT NULL DEFAULT now()
23
+);
24
+
25
+CREATE INDEX auth_audit_log_actor_id_idx   ON auth_audit_log (actor_id);
26
+CREATE INDEX auth_audit_log_target_idx     ON auth_audit_log (target_type, target_id);
27
+CREATE INDEX auth_audit_log_action_idx     ON auth_audit_log (action);
28
+CREATE INDEX auth_audit_log_created_at_idx ON auth_audit_log (created_at DESC);
29
+
30
+-- +goose Down
31
+DROP TABLE IF EXISTS auth_audit_log;