fix(Extract-NTHashes): add HMAC-SHA256 to the AES export (breaking format bump to ELY2)

Protect-FileWithAES used AES-256-CBC with no integrity check. A wrong
passphrase or corrupted/tampered ciphertext just decrypts to garbage
(or an unhelpful padding exception) instead of being detected.

Derives a second 32-byte key from the same PBKDF2 stream (the AES key
and HMAC key are sequential, non-overlapping ranges of one
Rfc2898DeriveBytes instance) and computes HMAC-SHA256 over
magic+salt+iv+ciphertext (encrypt-then-MAC), appended as a trailer.
Bumped the format magic from 'ELY1' to 'ELY2' so old and new files
are distinguishable.

This is a breaking change for whatever external tooling decrypts
these exports (this repo only ever encrypts - decryption happens on
a separate air-gapped machine per the README's FAQ) - documented the
new layout and the break in the README.

Verified with an isolated round-trip test (function extracted,
dot-sourced, paired with a hand-written decrypt+HMAC-verify): correct
passphrase round-trips cleanly, a wrong passphrase is rejected via
HMAC mismatch, and a single flipped ciphertext byte is also rejected
via HMAC mismatch, in all cases before any AES decryption is
attempted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:10:04 +02:00
co-authored by Claude Sonnet 5
parent 80199ad7d6
commit 5b761d8d56
2 changed files with 22 additions and 23 deletions
+20 -23
View File
@@ -7,7 +7,7 @@
##################################################
## Project: Elysium ##
## File: Extract-NTHashes.ps1 ##
## Version: 2.4.5 ##
## Version: 2.4.6 ##
## Support: support@cqre.net ##
##################################################
@@ -134,8 +134,15 @@ function Protect-FileWithAES {
$salt = New-Object byte[] 16
$rng.GetBytes($salt)
# Derive two independent keys from one PBKDF2 byte stream: the first 32 bytes for AES-256, the
# next 32 for HMAC-SHA256 (Rfc2898DeriveBytes.GetBytes returns a continuous stream across
# calls on the same instance, so these two ranges never overlap). CBC alone gives no integrity
# check - tampered or corrupted ciphertext just decrypts to garbage (or throws an unhelpful
# padding exception) instead of being detected. Encrypt-then-MAC over magic+salt+iv+ciphertext
# (format 'ELY2') catches both. Older 'ELY1' files this script produced have no MAC.
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Passphrase, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)
$key = $kdf.GetBytes(32)
$aesKey = $kdf.GetBytes(32)
$hmacKey = $kdf.GetBytes(32)
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize = 256
@@ -143,34 +150,24 @@ function Protect-FileWithAES {
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.GenerateIV()
$iv = $aes.IV
$encryptor = $aes.CreateEncryptor($key, $iv)
$fileStream = [System.IO.File]::Open($InputFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
$outFileStream = [System.IO.File]::Create($OutputFile)
$encryptor = $aes.CreateEncryptor($aesKey, $iv)
$hmac = [System.Security.Cryptography.HMACSHA256]::new($hmacKey)
try {
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY1')
$outFileStream.Write($magic, 0, $magic.Length)
$outFileStream.Write($salt, 0, $salt.Length)
$outFileStream.Write($iv, 0, $iv.Length)
$plainBytes = [System.IO.File]::ReadAllBytes($InputFile)
$cipherBytes = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length)
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($outFileStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
try {
$buffer = New-Object Byte[] 8192
while (($read = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$cryptoStream.Write($buffer, 0, $read)
}
} finally {
$cryptoStream.FlushFinalBlock()
$cryptoStream.Close()
}
$magic = [System.Text.Encoding]::ASCII.GetBytes('ELY2')
$header = $magic + $salt + $iv
$mac = $hmac.ComputeHash($header + $cipherBytes)
[System.IO.File]::WriteAllBytes($OutputFile, ($header + $cipherBytes + $mac))
} finally {
$outFileStream.Close(); $fileStream.Close(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
$encryptor.Dispose(); $hmac.Dispose(); $aes.Dispose(); $rng.Dispose(); $kdf.Dispose()
}
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC): $OutputFile"
Write-Host "File has been encrypted (PBKDF2+AES-256-CBC+HMAC-SHA256): $OutputFile"
}
function Get-FileChecksum {