aeat.adapters.persistence.storage.blob_store._blob_store module

Encrypted, classification-aware blob store.

Blobs are persisted under a content-addressed layout rooted at Settings.aeat_blob_store_dir. Each blob carries a sidecar BlobManifest Envelope that pins the sensitivity classification, the wire-format SHA-256 (plaintext for CORPUS; ciphertext for every other class), the optional wrapped data-encryption key, the AEAD nonce, the original size, and the content type.

Two layouts are supported:

  • Plaintext (SensitivityClass.CORPUS only): blob bytes are written verbatim under blobs/<hex[:2]>/<hex> where <hex> is the plaintext SHA-256. The manifest records encryption=None.

  • Ciphertext (every other class): the substrate mints a 32-byte data-encryption key (DEK), encrypts the blob with AES-256-GCM keyed by the DEK, wraps the DEK using the MasterKeyProvider (also AES-256-GCM), and writes the ciphertext under the plaintext digest path blobs/<hex[:2]>/<hex>.enc. The manifest records the ciphertext SHA-256, the wrapped DEK, and the AEAD nonces; the master key never touches disk.

The repository’s public read API returns plaintext bytes; the sensitivity class drives whether decryption is performed under the hood. Manifests are Envelope[BlobManifest] instances stored under <hex>.manifest.json.

Attempting to write a blob whose declared classification is incompatible with its layout (e.g. tagging an arbitrary blob as CORPUS to skip encryption) raises ClassificationError. Missing blobs raise BlobNotFoundError. SHA-256 disagreement between the on-disk file and the manifest raises BlobIntegrityError.

class BlobManifest(**data)[source]

Bases: BaseModel

Frozen manifest record for one blob in the encrypted blob store.

Variables:
  • sha256_plaintext_hex – Lowercase hex digest of the original plaintext bytes. Always present.

  • sha256_ciphertext_hex – Lowercase hex digest of the ciphertext on disk. None for plaintext (CORPUS) blobs.

  • size_plaintext – Size of the original plaintext in bytes.

  • content_type – Free-form MIME type or descriptive label (e.g. application/pdf, application/json, application/octet-stream). The substrate does not parse or validate the value; it is recorded for forensic clarity only. Consumers that need a stricter classification should choose the appropriate SensitivityClass rather than relying on the content-type label.

  • classification – The SensitivityClass of the payload.

  • wrapped_dek – Per-blob data-encryption key, AES-256-GCM-wrapped with the master key, expressed as JSON-friendly EncryptionMetadata. None for plaintext blobs.

  • payload_metadata – AEAD metadata for the payload itself (nonce + algorithm). The ciphertext lives on disk; this field carries only the surrounding metadata. None for plaintext blobs.

Parameters:
sha256_plaintext_hex: str
sha256_ciphertext_hex: str | None
size_plaintext: int
content_type: str
classification: SensitivityClass
wrapped_dek: EncryptionMetadata | None
payload_metadata: EncryptionMetadata | None
class BlobReference(**data)[source]

Bases: BaseModel

Frozen public handle for one blob.

Variables:
  • sha256_plaintext_hex – Lowercase hex digest of the plaintext. This is the natural key for retrieval; the consumer computes it from the plaintext at write time.

  • classification – The classification used at write time. Required because the on-disk layout differs by class (plaintext vs ciphertext); the get path uses this to find the file.

Parameters:
sha256_plaintext_hex: str
classification: SensitivityClass
class EncryptedBlobStore(*, root_dir, master_key_provider=None)[source]

Bases: object

Repository for the at-rest, classification-aware blob store.

Parameters:
property root_dir: Path

Return the configured root directory.

put(plaintext, *, classification, content_type='application/octet-stream')[source]

Persist plaintext and return a reference for retrieval.

The on-disk layout is dictated by classification: CORPUS-class blobs are written verbatim; every other class is encrypted with a fresh per-blob DEK that is then wrapped with the master key.

Parameters:
  • plaintext (bytes) – Bytes to persist.

  • classification (SensitivityClass) – SensitivityClass controlling the at-rest treatment (plaintext for CORPUS, ciphertext for all other classes).

  • content_type (str) – Stable MIME-type-style label stored in the manifest.

Return type:

BlobReference

Returns:

A BlobReference keyed by the plaintext SHA-256 and the classification.

Raises:

ClassificationError – If a non-CORPUS class is requested but the policy table forbids ciphertext for it (defensive check; the default table never trips this).

get(reference)[source]

Return the plaintext bytes for reference.

Parameters:

reference (BlobReference) – The blob reference identifying the stored object.

Return type:

bytes

Returns:

Decrypted plaintext bytes for the referenced blob.

Raises:

BlobNotFoundError – When the manifest or payload file is missing.

delete(reference)[source]

Remove the blob and its manifest.

Order: payload bytes (plaintext or ciphertext) are unlinked first, then the manifest. If the payload unlink fails, the manifest is left in place so a subsequent get surfaces a BlobIntegrityError rather than a silent BlobNotFoundError (sec-M-2).

Return type:

None

Parameters:

reference (BlobReference)

iter_manifests()[source]

Yield the BlobManifest of every blob currently persisted.

The walk is shallow: only the canonical blobs/<hex[:2]>/<hex>.manifest.json files are visited. Each manifest is loaded through a single-read + inline version-gate path; corrupted or unparseable manifests fail closed so corruption does not disappear from audit flows.

Return type:

Iterator[BlobManifest]

rotate_master_key(*, old_master_key_provider, new_master_key_provider)[source]

Re-wrap every blob’s per-record DEK under the new master key.

The blob store wraps each blob’s DEK directly under the master key (encrypt_record(dek, key=master_key, associated_data=_DEK_AAD)). When the master key rotates, every wrapped DEK must be re-wrapped under the new master key or the blob becomes unrecoverable.

Resume-idempotent: re-running on an already-rotated store decrypts the wrapped DEK under the new master key first; on success the manifest is skipped.

Parameters:
Return type:

tuple[int, int, int]

Returns:

A (rotated, skipped, errors) triple.