cadrumo.adapters.persistence.storage.sql._secure_object_row_codec module

Row-level codec helpers for SQL secure-object records.

This module keeps the encrypted row decode path close to the SQL secure-object adapter without leaving the algorithm embedded in the repository class. It validates row classification and exact schema identity before decrypting, and refuses rows whose revision hashes no longer match their stored metadata. Revision lineage is derived and stamped by the repository’s write funnel inside the same statement that persists the ciphertext.

See also

SecureObjectRepository

Repository that owns the write funnel and delegates row decoding here.

derive_revision_id()

Deterministic revision-id primitive the write funnel stamps rows with.

verify_revision_self_consistency()

Integrity check applied before decrypting an existing row.

build_revision_ancestor_ids()

Revision-lineage helper used to persist ancestor chains.

SecureObjectRecord

Plaintext record returned after classification, schema, lineage, and AEAD checks pass.

secure_object_payload_aad()

Associated-data builder that binds ciphertext to row identity.

SensitivityClass

Expected row classification validated before a row is decoded.

decode_secure_object_row(*, namespace, row_id, object_key, classification_str, schema_version, written_at, payload_wire, revision_id, previous_revision_id, payload_hash, ciphertext_hash, previous_payload_hash, expected_class, max_supported_version, namespace_definition, enforce_registered_row_schema)[source]

Validate and decrypt one secure-object row, raising on every failure.

The single decode pipeline behind both read surfaces. It always RAISES; choosing between propagating that failure and converting it to an unreadable row is the caller’s policy, so the two surfaces cannot drift in WHAT they check or in WHICH ORDER, only in how they report.

Order is part of the contract, and is deliberately integrity-before-crypto:

  1. classification is a known value, and is the one this namespace expects

  2. the stored schema version is readable, and registered for the namespace

  3. revision lineage is self-consistent

  4. the AEAD opens

  5. the current-version plaintext is returned

Step 3 runs before step 4 on purpose: a row whose stored lineage metadata already contradicts itself is refused without spending an AEAD open on it. That metadata is UNAUTHENTICATED until step 4 verifies the tag, so the pre-decrypt check is strictly refuse-only – it can reject a row, and it can do nothing else. It never marks a row readable, never skips a later step, and never selects a decode branch, so hostile header metadata cannot buy an attacker anything beyond a refusal they could have caused anyway.

Parameters:
  • namespace (str) – The secure-object namespace the row was stored under; it selects the registered row schema and scopes the diagnostics.

  • row_id (int) – The row’s primary key, carried only so a refusal can name the offending row.

  • object_key (bytes) – The row’s opaque object key, used as AEAD associated data so a payload cannot be replayed under a different key.

  • classification_str (str) – The classification exactly as stored. Parsed in step 1 and matched against expected_class.

  • schema_version (int) – The payload schema version as stored. Checked for exact readability and namespace registration in step 2.

  • written_at (datetime) – The row’s stored write timestamp, carried onto the decoded record.

  • payload_wire (bytes) – The stored wire bytes – the sealed envelope the AEAD opens in step 4.

  • revision_id (str | None) – The row’s own revision identifier, or None when the row carries no revision metadata.

  • previous_revision_id (str | None) – The revision this row supersedes, or None for a chain head.

  • payload_hash (str | None) – Digest of this row’s plaintext payload, or None.

  • ciphertext_hash (str | None) – Digest of this row’s wire bytes, or None.

  • previous_payload_hash (str | None) – Digest of the superseded row’s payload, or None. Together with the two revision ids this forms the lineage triple checked in step 3.

  • expected_class (SensitivityClass) – The SensitivityClass this namespace declares; the stored row’s own classification must match it exactly.

  • max_supported_version (int) – The highest schema version this reader can handle. A row on either side is refused rather than guessed at.

  • namespace_definition (SecureObjectNamespaceDefinition | None) – The namespace’s registered definition, or None when the namespace is unregistered.

  • enforce_registered_row_schema (Callable[..., None]) – The namespace-registration check invoked in step 2. Injected so the codec does not reach back into the registry, and so tests can drive registration failures directly.

Raises:
Return type:

SecureObjectRecord

secure_object_record_from_row(row, *, expected_class, max_supported_version, namespace_definition=None, enforce_registered_row_schema)[source]

Decode one ORM row, propagating the typed failure.

Fail-closed policy: a caller loading a specific row wants the typed error, not a placeholder. Normalisation of the ORM row is the only thing this wrapper adds; every check lives in decode_secure_object_row().

Parameters:
  • row (SecureObjectRow) – The ORM row to decode. Its columns are coerced to the concrete types the codec requires (int id, bytes object key and payload) and forwarded; nothing is otherwise interpreted here.

  • expected_class (SensitivityClass) – The SensitivityClass the row’s own classification must match, forwarded verbatim to decode_secure_object_row().

  • max_supported_version (int) – The highest schema version this reader can handle, forwarded verbatim.

  • namespace_definition (SecureObjectNamespaceDefinition | None) – The namespace’s registered definition, or None when unregistered, forwarded verbatim.

  • enforce_registered_row_schema (Callable[..., None]) – The namespace-registration check, forwarded verbatim.

Return type:

SecureObjectRecord

secure_object_list_item_from_raw_row(raw, *, namespace, expected_class, max_supported_version, namespace_definition, enforce_registered_row_schema)[source]

Decode one raw iter_records_with_failures row into a typed outcome.

expected_class is the SensitivityClass required for the namespace.

Fault-isolated: every failure mode (unknown classification, classification mismatch, unreadable schema version, revision-lineage inconsistency, decrypt failure, upgrade failure) returns a SecureObjectUnreadable carrying the reason instead of raising, so a caller iterating many rows can attribute a failure to its own row and keep inspecting the rest.

Fault isolation is the ONLY thing this wrapper adds to decode_secure_object_row(); the checks and their order live there, so the batch surface cannot validate a row differently from the single-row surface. Note SecureObjectUnreadableError subclasses DecryptionError and so must be caught first.

Return type:

SecureObjectRecord | SecureObjectUnreadable

Parameters: