انتقل إلى المحتوى الرئيسي

Class: SubjectKeys

Defined in: packages/persistence/src/personal/subject-keys.ts:246

Per-subject data keys: each subject that holds personal data has its own AES-256-GCM key, stored wrapped in subject_key (D1.9). This class gets or creates one, unwraps it through KeyWrap, and keeps it in a small in-memory cache (TTL 60 s by default, bounded, keyed by subject).

No port call happens inside a transaction (28, section 3). get reads the row in one short transaction, closes it, unwraps, and for a missing key wraps before the insert transaction opens. A caller that needs the key inside its own transaction resolves it first, or uses prepare and insert.

Erasure is eventually consistent across processes: destroySubjectKey evicts this process's cache once the destruction commits; another replica serves a cached key for up to the TTL. There is one credential replica through slice 6, and an event covers a second.

The table is subject_key, in the module's schema (the role's search_path supplies it), joined to subject for the scope check, so a scope reaches only subjects in its subtree. A cached key carries its subject's path and is served only to a minted scope that reaches it, so the cache never widens what a scope can read.

Constructors​

Constructor​

new SubjectKeys(options): SubjectKeys

Defined in: packages/persistence/src/personal/subject-keys.ts:256

Parameters​

options​

SubjectKeysOptions

Returns​

SubjectKeys

Accessors​

activeKekId​

Get Signature​

get activeKekId(): string

Defined in: packages/persistence/src/personal/subject-keys.ts:272

Returns​

string

The id of the key encryption key new data keys are wrapped under, and the one rewrap moves keys onto. It names a key and holds no secret.


cached​

Get Signature​

get cached(): number

Defined in: packages/persistence/src/personal/subject-keys.ts:264

Returns​

number

How many keys are cached now, expired ones included until they are touched.

Methods​

countInSoftwareCustody()​

countInSoftwareCustody<DB>(db, scope): Promise<number>

Defined in: packages/persistence/src/personal/subject-keys.ts:545

Counts the live keys that are in software custody (soft: ids), across every tenant. For one use: a process whose KeyWrap is on a token cannot unwrap those, so it says at start how many people's data it cannot read until the re-wrap has run. One query, no port call, and it returns a number: no key id leaves it.

Type Parameters​

DB​

DB

Parameters​

db​

Database<DB>

The module's pool.

scope​

SubjectKeyRewrapScope

A platform scope minted for SUBJECT_KEY_REWRAP_GRANT.

Returns​

Promise<number>

How many live keys carry a soft: id.

Throws​

forged_scope when the scope was not minted, or was minted for another grant.


destroySubjectKey()​

destroySubjectKey<DB>(tx, subjectId, audit): Promise<DestroyOutcome>

Defined in: packages/persistence/src/personal/subject-keys.ts:395

Erases a subject's personal data by destroying its key: sets wrapped_dek to NULL and destroyed_at, and runs audit in the same transaction, so the destruction and its record commit together or not at all. Calls no port: the caller opened tx with withScope, and nothing here wraps or unwraps.

The cache is touched only for a key that was destroyed, and only once tx has committed (TenantTransaction.afterCommit): the cached key is evicted and its handle revoked, and the subject is held out of the cache for one TTL, so a read that saw the live row before the commit cannot put it back. absent, already_destroyed and a destruction that rolls back leave the cache and every handle as they were: a key that still opens is never reported as erased. Until the commit, a concurrent read is served the key, which is still live.

Type Parameters​

DB​

DB

Parameters​

tx​

TenantTransaction<DB>

A transaction from withScope; its scope must reach the subject.

subjectId​

string

The subject.

audit​

(tx) => Promise<void>

Writes the subject-key.destroy record to the module's outbox in tx (AuditClient.record). It runs only when a key was destroyed, and must not call a port.

Returns​

Promise<DestroyOutcome>

destroyed; already_destroyed (idempotent, nothing recorded); or absent (no key, or not in scope: the same answer).


evict()​

evict(subjectId): void

Defined in: packages/persistence/src/personal/subject-keys.ts:564

Drops a subject's cached key and revokes the handle, so one a caller still holds answers erased rather than opening anything.

Parameters​

subjectId​

string

The subject.

Returns​

void


get()​

get<DB>(db, scope, subjectId, options?): Promise<SubjectKey | ErasedSubjectKey | undefined>

Defined in: packages/persistence/src/personal/subject-keys.ts:287

Resolves a subject's key.

Type Parameters​

DB​

DB

Parameters​

db​

Database<DB>

The module's pool.

scope​

Scope

A scope minted by messaging; the subject must be in its subtree.

subjectId​

string

The subject.

options?​

create: true makes the key when the subject has none.

create?​

boolean

Returns​

Promise<SubjectKey | ErasedSubjectKey | undefined>

The key; { erased: true } when it was destroyed; undefined when there is none and create is not set.

Throws​

outside_scope when the subject is not in the scope's subtree (on create); forged_scope.

Throws​

key_unavailable when custody could not unwrap the stored key.


insert()​

insert<DB>(tx, subjectId, prepared): Promise<SubjectKey>

Defined in: packages/persistence/src/personal/subject-keys.ts:359

Stores a prepared key in the caller's transaction: SQL only, no port call, so it commits with the subject it belongs to. Nothing is cached; a rollback leaves no trace, and the first get after commit caches it. The returned handle is for sealing the subject's first fields in the same transaction.

Type Parameters​

DB​

DB

Parameters​

tx​

TenantTransaction<DB>

A transaction from withScope.

subjectId​

string

The subject, which must exist and lie within the scope.

prepared​

PreparedSubjectKey

From prepare; usable once.

Returns​

Promise<SubjectKey>

The key, for sealing in this transaction.

Throws​

key_exists when the subject already has a key row.

Throws​

outside_scope when the subject is absent or not in the subtree.


keyState()​

keyState<DB>(tx, subjectId): Promise<"destroyed" | "absent" | "live">

Defined in: packages/persistence/src/personal/subject-keys.ts:430

Whether a subject's key has been destroyed, read in the caller's transaction (no cache): the check a caller makes before acting on an erased answer it got from a handle, which is final only once the destruction committed.

Type Parameters​

DB​

DB

Parameters​

tx​

TenantTransaction<DB>

A transaction from withScope; its scope must reach the subject.

subjectId​

string

The subject.

Returns​

Promise<"destroyed" | "absent" | "live">

destroyed; live; or absent (no key, or not in scope: the same answer).


prepare()​

prepare(): Promise<PreparedSubjectKey>

Defined in: packages/persistence/src/personal/subject-keys.ts:336

Generates a data key and wraps it, storing nothing. Call it before opening the transaction that will insert it.

Returns​

Promise<PreparedSubjectKey>

A key ready to store.

Throws​

Custody refused the wrap.


rewrap()​

rewrap<DB>(db, scope, options?): Promise<RewrapBatch>

Defined in: packages/persistence/src/personal/subject-keys.ts:481

Moves one batch of live keys that are not under the active key encryption key onto it: the re-wrap from software custody to a token (D3.14), and later rotation. The data keys do not change, only what protects them at rest, so every sealed field stays readable and no cache anywhere goes stale.

It runs across every tenant, which is why it takes a platform scope, and only one minted for SUBJECT_KEY_REWRAP_GRANT. A caller loops, passing each batch's cursor as the next after, until done or stopped.

What a batch does. It reads up to limit keys in one short transaction and closes it. For each key, with no transaction open (28, section 3), it unwraps under the key the row names, wraps under the active one, unwraps the result again and compares it with what it started from, then zeroes the data key. A plaintext data key exists for those three calls and no longer. The stored value is the only copy of a person's key, so the comparison is what stands between a faulty module and data nobody can ever read again; it costs one call to the token for each key. Last, one transaction stores the whole batch, each row only if it still holds the value that was read and has not been destroyed, and runs options.record so the batch's audit records commit with it. A run killed at any point therefore leaves no moved key without its record. Damaged keys are recorded the same way, even when the batch moved nothing.

A batch finishes or writes nothing. A stop of any kind, or a kill, leaves every row of the batch as it was, and the next call takes the same rows. A second run over finished work reads nothing and reports nothing, which is what makes the command idempotent. A destroyed key is never read and could not be written: migration 0103's trigger refuses it whatever this code does.

Concurrent runs are safe without a lock. The loser's updates match no row and are counted as changed; no key is wrapped under a value that was not read.

The read finds its rows by a scan in key order, which is fine for the estates slice 3 runs. Before rotation at national scale it wants an index on live keys by kek_id.

Type Parameters​

DB​

DB

Parameters​

db​

Database<DB>

The module's pool.

scope​

SubjectKeyRewrapScope

A platform scope minted for SUBJECT_KEY_REWRAP_GRANT.

options?​

RewrapOptions<DB> = {}

The reader, the cursor, the batch size and a signal.

Returns​

Promise<RewrapBatch>

What the batch did, as counts and subject ids.

Throws​

forged_scope when the scope was not minted, or was minted for another grant; invalid_input for a limit outside 1 to REWRAP_BATCH_MAX.