fix(intunewin): compute HMAC over IV+ciphertext, not ciphertext alone

The reference implementation (svrooij/ContentPrep Zipper.cs DecryptFileAsync)
reads the first 32 bytes as the stored HMAC, then hashes the *remaining* bytes
— i.e. IV (16 bytes) || ciphertext — to verify integrity. ImpTune was computing
HMAC(mac_key, ciphertext) which omits the IV. Intune's server-side HMAC check
would therefore always fail, manifesting as the same silent symptom as the
Detection.xml bug: empty wizard fields, greyed OK button, no error banner.

The blob layout is unchanged: [HMAC(32)] + [IV(16)] + [ciphertext].
Only the hash input is corrected: iv + ciphertext instead of ciphertext.

The Mac field in Detection.xml is also updated accordingly (it stores the same
HMAC value that is prepended to the blob).

Tests updated: test_hmac_matches now verifies HMAC over blob[32:] (= IV+ciphertext),
which is exactly what the reference decryption algorithm verifies against.

All 114 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 12:08:43 +02:00
co-authored by Claude Sonnet 4.6
parent 44a4f2c573
commit 74535ea089
2 changed files with 18 additions and 7 deletions
+7 -2
View File
@@ -77,8 +77,13 @@ def build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None:
cipher = AES.new(aes_key, AES.MODE_CBC, iv) cipher = AES.new(aes_key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size)) ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
# --- Step 4: Compute HMAC-SHA256 over ciphertext (using mac_key) --- # --- Step 4: Compute HMAC-SHA256 over (IV + ciphertext) using mac_key ---
mac_digest = hmac.new(mac_key, ciphertext, hashlib.sha256).digest() # The reference (svrooij/ContentPrep Zipper.cs DecryptFileAsync) reads the first
# 32 bytes as the stored HMAC, then computes the hash of the *remaining* bytes
# (= IV || ciphertext) to verify integrity. Authenticated-encryption best
# practice (Encrypt-then-MAC) also requires the IV to be covered by the MAC so
# that a forged IV cannot redirect decryption.
mac_digest = hmac.new(mac_key, iv + ciphertext, hashlib.sha256).digest()
# --- Step 5: Assemble encrypted blob: [HMAC(32)] + [IV(16)] + [ciphertext] --- # --- Step 5: Assemble encrypted blob: [HMAC(32)] + [IV(16)] + [ciphertext] ---
encrypted_blob = mac_digest + iv + ciphertext encrypted_blob = mac_digest + iv + ciphertext
+11 -5
View File
@@ -191,27 +191,33 @@ class TestEncryptedBlobLayout:
class TestCryptographicVerification: class TestCryptographicVerification:
def test_hmac_matches(self, package_contents): def test_hmac_matches(self, package_contents):
"""HMAC-SHA256 of ciphertext matches first 32 bytes of blob AND Mac in Detection.xml.""" """HMAC-SHA256 of (IV || ciphertext) matches first 32 bytes of blob AND Mac in Detection.xml.
The reference implementation (svrooij/ContentPrep Zipper.cs DecryptFileAsync) reads
the first 32 bytes as the stored HMAC, then computes the hash of the *remaining* bytes
(bytes[32:] = IV || ciphertext) to verify integrity. The IV MUST be included in the
HMAC so that a forged IV cannot redirect decryption without being detected.
"""
blob = package_contents["blob"] blob = package_contents["blob"]
tree = package_contents["tree"] tree = package_contents["tree"]
blob_hmac = blob[:32] blob_hmac = blob[:32]
ciphertext = blob[48:] iv_and_ciphertext = blob[32:] # IV (16 bytes) + ciphertext — what the reference hashes
mac_key_b64 = _get_enc_text(tree, "MacKey") mac_key_b64 = _get_enc_text(tree, "MacKey")
mac_key = base64.b64decode(mac_key_b64) mac_key = base64.b64decode(mac_key_b64)
computed_hmac = hmac.new(mac_key, ciphertext, hashlib.sha256).digest() computed_hmac = hmac.new(mac_key, iv_and_ciphertext, hashlib.sha256).digest()
# Must match the blob header # Must match the blob header
assert computed_hmac == blob_hmac, ( assert computed_hmac == blob_hmac, (
"HMAC-SHA256 of ciphertext does not match the first 32 bytes of the blob" "HMAC-SHA256 of (IV || ciphertext) does not match the first 32 bytes of the blob"
) )
# Must also match Detection.xml Mac field # Must also match Detection.xml Mac field
xml_mac = base64.b64decode(_get_enc_text(tree, "Mac")) xml_mac = base64.b64decode(_get_enc_text(tree, "Mac"))
assert computed_hmac == xml_mac, ( assert computed_hmac == xml_mac, (
"HMAC-SHA256 of ciphertext does not match the Mac value in Detection.xml" "HMAC-SHA256 of (IV || ciphertext) does not match the Mac value in Detection.xml"
) )
def test_decryption_roundtrip(self, source_dir, package_contents): def test_decryption_roundtrip(self, source_dir, package_contents):