Files
Alarm/database/migrations/003_hash_device_id.sql
T
Admin 0334d7fe93 [feat](alarm): store device ids as sha256 digests
device_id is a stable pseudonymous idempotency key, not a credential; a database dump should not be a usable list of device identifiers. Hash every caller-supplied value before it reaches the table, keep raw values out of logs and subscription views, and migrate existing rows with the identical SQL encoding.
2026-10-19 02:26:31 +01:00

47 lines
2.1 KiB
SQL

\c dcntrlized;
-- 003_hash_device_id.sql — store SHA-256 digests of device ids, not the values.
--
-- Why: alarm.subscriptions.device_id is a stable pseudonymous client value
-- (dev_ + 96 random bits). It is an idempotency key, not a credential, but a
-- database dump should not be a usable list of device identifiers. The service
-- hashes every caller-supplied value before it touches the table, so new rows
-- are already stored as 'sha256:<64 lowercase hex>'.
--
-- Why this does not break idempotency (the thing someone will worry about):
-- SHA-256 equality is exactly equality of the raw values. Two raw ids still
-- collide iff their digests do, so the
-- subscriptions_alert_user_device_key UNIQUE (alert_id, subscriber_user_id,
-- device_id) constraint keeps its meaning and the re-join lookup still finds the
-- same row. The hash is one-way, so the raw value cannot be recovered from it —
-- which is the point.
--
-- The encoding matches Alarm/src/internal/deviceid.Hash exactly:
-- 'sha256:' || encode(sha256(convert_to(device_id,'UTF8')),'hex')
-- and is self-describing: a value starting with 'sha256:' is a digest, never a
-- client id. Stored length is 71 bytes, inside the column's VARCHAR(128).
--
-- sha256(bytea) is a core PostgreSQL function since 11 (no pgcrypto needed); the
-- dev database is 16.14. The migration is guarded on that anyway: if sha256 is
-- somehow unavailable it fails loudly here rather than storing raw ids mixed
-- with digests.
--
-- Idempotent: the NOT LIKE 'sha256:%' predicate skips rows already hashed, so
-- re-applying the sequence is a clean no-op (same pattern as 002).
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'alarm') THEN
RETURN;
END IF;
IF to_regprocedure('sha256(bytea)') IS NOT NULL THEN
UPDATE alarm.subscriptions
SET device_id = 'sha256:' || encode(sha256(convert_to(device_id, 'UTF8')), 'hex')
WHERE device_id NOT LIKE 'sha256:%';
ELSE
RAISE EXCEPTION
'sha256(bytea) is unavailable; device_id hashing would store mixed raw/digest rows';
END IF;
END
$$;