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:
@@ -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
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user