synology_apm_repo.sdk.format.crypto module¶
Chunk and key-material cryptography (FORMAT-SPEC.md: key-hierarchy, chunk-pool-encryption, vaultkey-custody, aHlT).
Three independent AES schemes, never confused with each other:
Chunk pool (
.buk): AES-256-CTR, key =vaultKey(the DEK), IV derived from each chunk’s ownChunkAddress— never random, never stored, recomputed identically on every read (chunk-pool-encryption).Wrapped VaultKey: AES-256-GCM, key =
userKey(the KEK), nonce = the first 12 ASCII bytes ofuserKeyID(vaultkey-custody) — this is the only place a nonce is explicit rather than derived, and it doubles as an integrity check (a wrong key/nonce pair fails the GCM tag outright).copy_meta_file’saHlTenvelope reuses the chunk-pool DEK (vaultKey) but with AES-256-CTR and an IV stored in its own header, not derived (aHlT).db/copy_target_version.version_spec: AES-256-CTR, same DEK (vaultKey) again, but its own derived-not-stored IV, taken from the first 16 ASCII bytes ofhex(MD5(version_uid))rather than the raw MD5 digest (version-spec-encryption).
Nothing here decides whether something is encrypted — that is always a mode-bit or magic-byte check made by the caller; this module only ever runs once the caller has already decided encryption applies.
- synology_apm_repo.sdk.format.crypto.NO_ENCRYPTION_USER_KEY_ID = 'NoEncryption'¶
user_key_idvalue marking a vault that was never encrypted.
- synology_apm_repo.sdk.format.crypto.chunk_iv(addr)¶
16-byte AES-CTR IV for one chunk: its own 64-bit
ChunkAddress, big-endian, repeated twice (FORMAT-SPEC.md: chunk-pool-encryption).
- synology_apm_repo.sdk.format.crypto.decrypt_chunk(vault_key, addr, ciphertext)¶
AES-256-CTR-decrypt one chunk’s ciphertext. Does not decompress — see
compressionfor that.ciphertextaccepts amemoryviewas well asbytes— a caller slicing chunk ciphertext straight out of a merged run’s I/O buffer can pass a view through without copying, sinceCipher.decryptor().update()reads it via the buffer protocol and always allocates its own fresh plaintext output either way.One ``Cipher`` per call, deliberately. Each chunk’s counter starts from its own independently derived
chunk_iv, not from where the previous chunk’supdate()left the running counter, so streaming many chunks through one decryptor would silently produce wrong plaintext after the first. Batching theCipherconstruction itself is not a win with AES-NI either: chunk ciphertext lengths vary (compressed sizes, not a fixed 4096), so building a batch’s counter blocks needs a Python-level loop whose own overhead dwarfs whatever construction cost it would save. Thealgorithms.AESkey-schedule half is safe to reuse across chunks — see_aes_algorithm.
- synology_apm_repo.sdk.format.crypto.parse_key_string(key_string)¶
Split an administrator-provided key string
"<userKeyID>@<base64(userKey)>"(FORMAT-SPEC.md: key-hierarchy) into(user_key_id, user_key). Splits on the last@per spec, and validatesuser_key_idis exactly 12 characters anduser_keydecodes to exactly 32 raw bytes.Does not special-case
"NoEncryption"— that’s a caller-level (KeyMaterial) decision about whether a key is needed, not a string-parsing concern.
- synology_apm_repo.sdk.format.crypto.unwrap_vault_key(user_key_id, user_key, wrapped)¶
AES-256-GCM-unwrap a 48-byte wrapped VaultKey (
ciphertext(32) || tag(16)) usinguserKeyand a nonce built fromuserKeyID’s first 12 ASCII bytes (FORMAT-SPEC.md: vaultkey-custody).A successful unwrap already proves the
(userKeyID, userKey)pair is correct for this repository’s real data too, not merely for this stored record in isolation — the DEK it unwraps never changes after first initialization.- Raises:
KeyMismatchError – The GCM tag check fails — proof the
(userKeyID, userKey)pair does not match.
- synology_apm_repo.sdk.format.crypto.ahlt_decrypt(data, vault_key)¶
Decrypt a
copy_meta_fileentry’saHlT-enveloped bytes: 64-byte header (the genericIndexHeadershell, IV at[8,24)) followed by AES-256-CTR ciphertext from byte 64 onward (FORMAT-SPEC.md: aHlT). Returns the plaintext body only (header stripped).The IV here is stored in the header, unlike a pool chunk’s address-derived IV — do not reuse
chunk_ivfor this envelope.
- synology_apm_repo.sdk.format.crypto.version_spec_iv(version_uid)¶
16-byte AES-CTR IV for one
copy_target_versionrow’s ownversion_speccolumn: the first 16 ASCII bytes of the hex stringhex(MD5(version_uid))— not the raw 16-byte MD5 digest itself. MD5’s raw digest is also exactly 16 bytes, which makes grabbing it directly (instead of its hex text) an easy trap: the on-disk format takes the hex string and copies its first 16 characters, never the raw digest bytes (FORMAT-SPEC.md: version-spec-encryption).
- synology_apm_repo.sdk.format.crypto.decrypt_version_spec(ciphertext_b64, version_uid, vault_key)¶
Decrypt one
copy_target_version.version_speccolumn: AES-256-CTR, same DEK as chunk-pool/aHlT(vault_key), IV fromversion_spec_iv. Plain standard base64 text, no header/magic bytes to sniff — whether to call this at all is the caller’s own decision (repo.vault_key is not None), same “no guessing” rule as this module’s other schemes (FORMAT-SPEC.md: version-spec-encryption).Returns the decoded UTF-8 plaintext (a JSON string) — raises
UnicodeDecodeError/binascii.Erroron a wrong key or corrupt input rather than returning garbage silently; callers decide how to degrade.