aegis-api/Scripts/first_migrations.sql

287 lines
14 KiB
SQL

-- ============================================================
-- AEGIS MANIFEST (SQLite) - UPDATED: datastore label visible, inner requires PIN
-- ============================================================
-- ----------------------------
-- PRAGMAS (aplicar na abertura da conexão)
-- ----------------------------
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA temp_store = MEMORY;
-- ============================================================
-- USERS (OIDC identities)
-- ============================================================
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY, -- GUID/ULID (string)
created_at INTEGER NOT NULL, -- epoch seconds
disabled_at INTEGER, -- null = active
-- OIDC identity (stable pairing)
subject TEXT NOT NULL, -- "sub"
issuer TEXT NOT NULL, -- "iss"
display_name TEXT,
email TEXT,
UNIQUE(subject, issuer)
);
-- ============================================================
-- DATASTORES
-- ============================================================
-- IMPORTANT:
-- - datastore name is decryptable WITHOUT PIN (using Label Key mechanism)
-- - datastore inner content requires PIN (Master Key unwrapped with PIN-derived KEK)
CREATE TABLE IF NOT EXISTS datastores (
datastore_id TEXT PRIMARY KEY, -- GUID/ULID
created_at INTEGER NOT NULL,
deleted_at INTEGER,
-- --------------------------
-- LABEL (visible without PIN)
-- --------------------------
-- name_enc is encrypted with a "Label Key" (LK) that does NOT require datastore PIN.
-- LK can be derived from a server secret, KMS, or any non-PIN mechanism.
name_enc BLOB NOT NULL,
name_nonce BLOB NOT NULL,
-- Identifies which LK mechanism/version was used (for rotation/migration).
-- Examples: "lk:server:v1", "lk:kms:key/123", "lk:yubikey:piv:9c"
lk_kid TEXT NOT NULL,
lk_version INTEGER NOT NULL DEFAULT 1,
-- --------------------------
-- MASTER KEY (PIN protected)
-- --------------------------
-- mk_wrapped is the datastore Master Key (MK) encrypted ("wrapped") with a KEK derived from the datastore PIN.
mk_wrapped BLOB NOT NULL,
mk_nonce BLOB NOT NULL,
mk_version INTEGER NOT NULL DEFAULT 1,
-- PIN KDF parameters (so you can change KDF settings per datastore over time)
pin_kdf TEXT NOT NULL DEFAULT 'argon2id', -- 'argon2id' recommended
pin_salt BLOB NOT NULL,
-- Store params as JSON text (portable and easy to evolve)
-- Example: {"memory_kib":65536,"iterations":3,"parallelism":1,"output_len":32}
pin_kdf_params TEXT NOT NULL,
-- Optional: quick verifier to reduce unwrap attempts (can be null).
-- If used, store an AEAD-encrypted constant (e.g., "AEGIS_PIN_OK") with KEK_pin and a nonce.
-- Otherwise you simply "try unwrap MK" and treat failure as "wrong PIN".
pin_verifier_enc BLOB,
pin_verifier_nonce BLOB,
-- Root node of the datastore tree
root_node_id TEXT NOT NULL,
FOREIGN KEY(root_node_id) REFERENCES nodes(node_id)
);
-- ============================================================
-- DATASTORE MEMBERS (authorization)
-- ============================================================
CREATE TABLE IF NOT EXISTS datastore_members (
datastore_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'OWNER'|'ADMIN'|'EDITOR'|'VIEWER'
created_at INTEGER NOT NULL,
PRIMARY KEY(datastore_id, user_id),
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id),
FOREIGN KEY(user_id) REFERENCES users(user_id)
);
-- ============================================================
-- NODES (logical tree) - INTERNAL: requires PIN
-- ============================================================
CREATE TABLE IF NOT EXISTS nodes (
node_id TEXT PRIMARY KEY, -- GUID/ULID
datastore_id TEXT NOT NULL,
parent_id TEXT, -- null only for root
kind INTEGER NOT NULL, -- 1=DIR, 2=FILE
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
deleted_at INTEGER, -- soft delete/trash
-- Encrypted node name (requires MK -> PIN)
name_enc BLOB NOT NULL,
name_nonce BLOB NOT NULL,
-- File-only fields (kind=FILE)
main_object_id TEXT, -- FK objects.object_id
size_plain INTEGER, -- optional (can leak info; keep or encrypt later)
mime_enc BLOB, -- optional: encrypt mime to reduce metadata leak
mime_nonce BLOB,
version INTEGER NOT NULL DEFAULT 1, -- optimistic lock
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id),
FOREIGN KEY(parent_id) REFERENCES nodes(node_id),
FOREIGN KEY(main_object_id) REFERENCES objects(object_id)
);
-- ============================================================
-- OBJECTS (encrypted blobs) - INTERNAL: requires PIN
-- ============================================================
CREATE TABLE IF NOT EXISTS objects (
object_id TEXT PRIMARY KEY, -- GUID/ULID
datastore_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
-- AEAD algorithm info
cipher TEXT NOT NULL, -- e.g. 'AES-256-GCM'
nonce BLOB NOT NULL, -- base nonce / object nonce (format depends on your file container)
-- DEK wrapped by datastore MK (requires PIN)
dek_wrapped BLOB NOT NULL,
dek_nonce BLOB NOT NULL,
-- Reference to blob location (also encrypted to avoid path leaks)
blob_ref_enc BLOB NOT NULL,
blob_ref_nonce BLOB NOT NULL,
size_cipher INTEGER NOT NULL,
hash_plain BLOB, -- optional (SHA-256) for integrity/dedupe v2
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id)
);
-- ============================================================
-- DERIVATIVES (thumbs/previews/posters/etc.) - INTERNAL: requires PIN
-- ============================================================
CREATE TABLE IF NOT EXISTS derivatives (
derivative_id TEXT PRIMARY KEY, -- GUID/ULID
datastore_id TEXT NOT NULL,
node_id TEXT NOT NULL, -- owning file node
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
kind TEXT NOT NULL, -- 'THUMB'|'POSTER'|'PREVIEW'|'WAVEFORM'|'SPRITE'...
profile TEXT NOT NULL, -- '256w','512w','page1','t=3s','wave:1024',...
object_id TEXT, -- object containing the derivative blob (when READY)
status INTEGER NOT NULL, -- 0=PENDING,1=READY,2=FAILED
-- store error details encrypted (may contain sensitive paths/info)
error_enc BLOB,
error_nonce BLOB,
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id),
FOREIGN KEY(node_id) REFERENCES nodes(node_id),
FOREIGN KEY(object_id) REFERENCES objects(object_id),
UNIQUE(datastore_id, node_id, kind, profile)
);
-- ============================================================
-- JOBS (SQLite queue) - does NOT require PIN itself
-- Payload should be encrypted with MK if it contains sensitive info (recommended).
-- ============================================================
CREATE TABLE IF NOT EXISTS jobs (
job_id TEXT PRIMARY KEY, -- GUID/ULID
datastore_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
available_at INTEGER NOT NULL, -- for backoff scheduling
picked_at INTEGER,
finished_at INTEGER,
kind TEXT NOT NULL, -- 'GEN_DERIVATIVES'|'REWRAP_KEYS' etc.
payload_enc BLOB NOT NULL,
payload_nonce BLOB NOT NULL,
status INTEGER NOT NULL, -- 0=QUEUED,1=RUNNING,2=DONE,3=FAILED
attempts INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id)
);
-- ============================================================
-- AUDIT LOG (encrypted details) - INTERNAL: may require PIN depending on what you put in details
-- ============================================================
CREATE TABLE IF NOT EXISTS audit_log (
audit_id TEXT PRIMARY KEY, -- GUID/ULID
datastore_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
actor_user_id TEXT, -- null for system/worker
action TEXT NOT NULL, -- 'UPLOAD','RENAME','MOVE','DELETE','DOWNLOAD','UNLOCK','LOCK'...
target_node_id TEXT,
details_enc BLOB NOT NULL,
details_nonce BLOB NOT NULL,
FOREIGN KEY(datastore_id) REFERENCES datastores(datastore_id),
FOREIGN KEY(actor_user_id) REFERENCES users(user_id),
FOREIGN KEY(target_node_id) REFERENCES nodes(node_id)
);
-- ============================================================
-- INDEXES (hot paths)
-- ============================================================
-- list directory: by datastore + parent, excluding deleted
CREATE INDEX IF NOT EXISTS idx_nodes_parent
ON nodes(datastore_id, parent_id, deleted_at);
CREATE INDEX IF NOT EXISTS idx_nodes_parent_kind
ON nodes(datastore_id, parent_id, kind, deleted_at);
CREATE INDEX IF NOT EXISTS idx_nodes_main_object
ON nodes(main_object_id);
CREATE INDEX IF NOT EXISTS idx_objects_datastore_created
ON objects(datastore_id, created_at);
CREATE INDEX IF NOT EXISTS idx_derivatives_node
ON derivatives(datastore_id, node_id, kind, status);
-- jobs queue
CREATE INDEX IF NOT EXISTS idx_jobs_queue
ON jobs(status, available_at);
-- members lookup
CREATE INDEX IF NOT EXISTS idx_members_user
ON datastore_members(user_id);
-- datastore listing for a user typically joins members -> datastores
CREATE INDEX IF NOT EXISTS idx_members_datastore
ON datastore_members(datastore_id);
-- audit time queries
CREATE INDEX IF NOT EXISTS idx_audit_datastore_time
ON audit_log(datastore_id, created_at);
-- ============================================================
-- TRIGGERS (touch updated_at / version)
-- NOTE: SQLite default PRAGMA recursive_triggers is OFF, so these won't recurse infinitely.
-- ============================================================
CREATE TRIGGER IF NOT EXISTS trg_nodes_touch
AFTER UPDATE ON nodes
FOR EACH ROW
BEGIN
UPDATE nodes
SET updated_at = strftime('%s','now'),
version = version + 1
WHERE node_id = NEW.node_id;
END;
CREATE TRIGGER IF NOT EXISTS trg_derivatives_touch
AFTER UPDATE ON derivatives
FOR EACH ROW
BEGIN
UPDATE derivatives
SET updated_at = strftime('%s','now')
WHERE derivative_id = NEW.derivative_id;
END;