-- Per-user X25519 identity replacing the per-device key model. -- The private key is sealed with a PIN-derived Argon2id KEK; the server -- never sees plaintext. Lockout counters protect the 6-digit PIN against -- online brute force by gating ciphertext delivery (see try_unlock_user_key). create table if not exists public.user_keys ( user_id uuid primary key references auth.users(id) on delete cascade, public_key bytea not null, sealed_private_key bytea not null, salt bytea not null, kdf_params jsonb not null, recovery_sealed_private_key bytea null, recovery_salt bytea null, failed_attempts int not null default 0, locked_until timestamptz null, failed_recovery_attempts int not null default 0, recovery_locked_until timestamptz null, key_version int not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); alter table public.user_keys enable row level security; drop policy if exists user_keys_self_rw on public.user_keys; create policy user_keys_self_rw on public.user_keys for all to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid()); -- Public-key view: any authenticated user may read peer public keys to wrap -- conv-keys for them. Only the columns granted below are exposed. create or replace view public.user_public_keys with (security_invoker = true) as select user_id, public_key, key_version from public.user_keys; grant select on public.user_public_keys to authenticated; -- Column-level grant lets the view see those columns without RLS rejecting -- non-owners. The self_rw policy still grants full row access to the owner. grant select (user_id, public_key, key_version) on public.user_keys to authenticated; drop policy if exists user_keys_select_public_columns on public.user_keys; create policy user_keys_select_public_columns on public.user_keys for select to authenticated using (true); alter publication supabase_realtime add table public.user_keys;