Coding Stephan

Decrypting intunewin files

In my quest to build an open-source, cross platform, tool that packages Win32 apps from Winget for Intune WingetIntune on GitHub, I’ve been looking at the Win32 Content Prep Tool in my previous post. In this post I’ll try to decrypt the IntunePackage.intunewin file that is generated by the tool.

PowerShell module to create and decrypt .intunewin files

Goals for this post

In this post I’ll try to decrypt the IntunePackage.intunewin file that is generated by the Win32 content prep tool. I’ll use the guesses and generated code from my previous post.

Summary of my guesses:

  • Main .intunewin file is an unencrypted zip file
  • Contains a IntunePackage.intunewin file inside the Content folder, which is encrypted and probably a zip file
  • Contains a Detection.xml file which may contain data required to decrypt the IntunePackage.intunewin file
  • I guessed Authenticated encryption is used based on data inside the Detection.xml file.

TLDR;

The intunewin file is a zip file, which contains a Detection.xml file which contains the encryption keys and a IntunePackage.intunewin file, which is yet another zip file. But this time it’s AES-256 encrypted using a keyed hashed using HMAC-SHA256. Authenticated encryption.

Jump to the conclusion if you’re not interested in the actual code.

Decrypting the intunewin file

Before we start with the decryption, I would need to extract the main .intunewin package. The requirements still are (from my previous post):

  • Should work cross platform
  • Should never synchronously access IO (as by default this will not work in various environments)
  • Preferably no external dependencies
  • Target framework is .NETStandard 2.0 (so it can be used in PowerShell Core, among others).

To support some nice C# language features, I did set the Language version to 8.0 though, but that should not matter once compiled.

Extracting the intunewin file

Opening an asynchronous stream to the file is easy:

using (FileStream fileStream = new FileStream(packageFile, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 4096, useAsync: true))
{
    await Zipper.UnzipStreamAsync(fileStream, tempFolder, cancellationToken);
}

And unzipping a zip file loaded in the stream is also no rocket science, and is the most asynchronous possible. This code reads the zip file from the stream because it’s a bit more efficient than reading the whole file into memory first.

internal static async Task UnzipStreamAsync(Stream stream, string destinationFolder, CancellationToken cancellationToken)
{
    using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Read, false))
    {
        foreach (ZipArchiveEntry entry in archive.Entries)
        {
            if (cancellationToken.IsCancellationRequested)
                break;
            string directoryName = Path.Combine(destinationFolder, Path.GetDirectoryName(entry.FullName));
            if (!string.IsNullOrEmpty(directoryName))
                Directory.CreateDirectory(directoryName);
            if (entry.Length > 0L)
            {
                string destinationFileName = Path.Combine(destinationFolder, entry.FullName);
                using (Stream entryStream = entry.Open())
                using (FileStream fileStream = new FileStream(destinationFileName, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
                {
                    await entryStream.CopyToAsync(fileStream);
                    fileStream.Close();
                    entryStream.Close();
                }
            }
        }
    }
}

Read the Detection.xml file

To be able to extract the actual content of the file we need to read the Detection.xml file, as this contains the EncryptionInfo. I needed to create a StreamReader from the FileStream to be able to read the XML data correctly, this probably has something to do with the encoding of the file and the default encoding of the StreamReader.

var metadataFile = Path.Combine(tempFolder, "IntuneWinPackage", "Metadata", "Detection.xml");
ApplicationInfo? applicationInfo = null;
using (FileStream metadataStream = new FileStream(metadataFile, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 4096, useAsync: true))
using (StreamReader metadataReader = new StreamReader(metadataStream))
{
    applicationInfo = ApplicationInfo.FromXml(metadataReader);
    metadataReader.Close();
    metadataStream.Close();
}

if (applicationInfo == null || applicationInfo.EncryptionInfo == null)
    throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, "Could not read metadata file {0}", metadataFile));

This code also uses the following static method

// A StreamReader is in fact a TextReader, so we can use the XmlSerializer to deserialize the XML data.
internal static ApplicationInfo FromXml(TextReader textReader)
{
    return (ApplicationInfo)new XmlSerializer(typeof(ApplicationInfo)).Deserialize(textReader);
}

Encrypted filestream

We now (presumably) have the encryption info, and we can open the encrypted file stream:

var encryptedPackage = Path.Combine(tempFolder, "IntuneWinPackage", "Contents", applicationInfo.FileName);
logger.LogDebug("Decrypting {EncryptedPackage} to {OutputFolder}", encryptedPackage, outputFolder);
using (FileStream encryptedFileStream = new FileStream(encryptedPackage, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 4096, useAsync: true))
using (Stream decryptedStream = await Encryptor.DecryptFileAsync(encryptedFileStream, applicationInfo.EncryptionInfo.EncryptionKey!, applicationInfo.EncryptionInfo.MacKey!, cancellationToken))
{
    // Using the same unzip method as before, but now on the decrypted stream.
    await Zipper.UnzipStreamAsync(decryptedStream, outputFolder, cancellationToken);
    decryptedStream.Close();
    encryptedFileStream.Close();
    logger.LogInformation("Unpacked intunewin at {PackageFile} to {OutputFolder}", packageFile, outputFolder);
}

If you checked this code, you would see there is nothing fancy going on here. We just open the encrypted file stream, and pass it to the Encryptor.DecryptFileAsync method, where the actual magic happens, before passing it to the UnzipStreamAsync method shown before.

Encryptor.DecryptFileAsync

The Encryptor.DecryptFileAsync method is the one that does the actual decryption. It’s a bit more complex than the other methods, but it’s still not rocket science. Since we are talking about authenticated encryption it will also verify that hash and fail if there is a mismatch.

internal static async Task<Stream> DecryptFileAsync(Stream inputStream, string encryptionKey, string hmacKey, CancellationToken cancellationToken)
{
    var resultStream = new MemoryStream();
    // Get the encryptionKey and the hmacKey as byte arrays, since they are stored as base64 strings.
    var encryptionKeyBytes = Convert.FromBase64String(encryptionKey);
    var hmacKeyBytes = Convert.FromBase64String(hmacKey);
    using (Aes aes = Aes.Create())
    using (HMACSHA256 hmac = new HMACSHA256(hmacKeyBytes))
    {
        int offset = hmac.HashSize / 8;
        byte[] buffer = new byte[offset];
        // Read the first 32 bytes, which is the hash, this will also move the stream forward.
        await inputStream.ReadAsync(buffer, 0, offset, cancellationToken);
        // Compute the hash of the remaining bytes.
        byte[] hash = await hmac.ComputeHashAsync(inputStream, cancellationToken);
        // Compare the hashes, if they don't match, throw an exception.
        if (!buffer.CompareHashes(hash))
        {
            throw new InvalidDataException("Hashes do not match");
        }
        // Rewind the stream to the beginning of the encrypted data.
        inputStream.Seek(offset, SeekOrigin.Begin);
        byte[] iv = new byte[aes.IV.Length];
        // Read the IV from the stream, (next 16 bytes).
        await inputStream.ReadAsync(iv, 0, iv.Length, cancellationToken);
        // Create a cryptostream with a decryptor with the encryptionKey and the IV.
        using (ICryptoTransform cryptoTransform = aes.CreateDecryptor(encryptionKeyBytes, iv))
        using (CryptoStream cryptoStream = new CryptoStream(inputStream, cryptoTransform, CryptoStreamMode.Read))
        {
            // Copy the decrypted data to the resultStream.
            await cryptoStream.CopyToAsync(resultStream, 2097152, cancellationToken);
            // Rewind the resultStream to the beginning.
            resultStream.Seek(0, SeekOrigin.Begin);
        }
    }
    // Return the resultStream, which still is a Stream which has to be closed by the caller.
    return resultStream;
}

Series: Intune

Conclusion

The educated guesses from the previous post were correct, the inner IntunePackage.intunewin file is indeed a zip file, and it’s encrypted using AES-256 and keyed hashed using HMAC-SHA256, so called Authenticated encryption. The Detection.xml file contains the encryption keys.

By learning how to decrypt the IntunePackage.intunewin file, and extracting the actual content. I now possess all the knowledge to build a cross platform version of the Win32 Content Prep Tool. I liked the challenge, since I never worked with aes based file encryption before, and I learned a lot about the inner workings of the Win32 Content Prep Tool.

This knowledge will be shared in the form of:

  1. An actual open-source C# library that can create and decrypt intunewin files, targeting the .NETStandard 2.0, for others to use in their applications.
  2. A PowerShell module that uses the library to decrypt intunewin files, and can be used to create intunewin files, a faster drop-in replacement for the current Content prep tool. This module will be published to the PowerShell Gallery, and will run on any platform.

Spoiler alert: I already started working on the library, and it’s already working. I’ll publish it to NuGet once I’m done with the PowerShell module. And boy can I tell you, it’s blazing fast because of the asynchronous IO code, for both creating the intunewin file and decrypting it.

Once this is done, I’ll update my WingetIntune tool to use the new library, and thus cutting the last Windows dependency from WingetIntune.

I hope you enjoyed this post, and if you have any feedback, please let me know on Twitter or LinkedIn. Some of the code you see in this post is generated by Github CoPilot, and then manually optimized to be fully asynchronous and cross platform. GitHub CoPilot helped me get started with the decryption code, but it still needed manual work to make it as performant as desired.