Microsoft Intuneでアプリケーションを配布する際に必要な.intunewinファイルですが、これまでMicrosoft公式ツールはWindows専用で、中身を確認する手段もありませんでした。

今回、Go言語でクロスプラットフォーム対応のintunewinファイル作成・解凍ツール、intunewinを開発しました。 その経緯と実装の詳細を紹介します。

intunewinファイルとは

.intunewinファイルは、Microsoft Intuneでアプリケーションを配布する際に使用される専用のパッケージフォーマットです。

Intuneでは、Win32アプリケーションを配布する場合、アプリケーションのインストーラーやファイル群を.intunewin形式に変換する必要があります。

このファイルは、以下のような特徴を持っています。

  • パッケージ(ディレクトリ)をZIP形式で圧縮
  • AES-256-CBCで暗号化
  • HMAC-SHA256による改ざん検証
  • メタデータ(暗号鍵、ファイルサイズ、ハッシュ値など)をXML形式で保存

ツールを作ろうと思ったきっかけ

Windows専用でクロスプラットフォームに対応していない

公式ツールはWindowsでしか動作しません。

macOSやLinuxを使用している場合、わざわざWindows環境を用意する必要があります。

また、GitHub ActionsなどのCI/CDサービスでは、Windowsランナーの実行コストがLinuxランナーと比較して2〜3倍高くなることが一般的です。 そのため、公式ツールをCI/CDに組み込むと、ビルドコストが大幅に増加してしまうという課題もありました。

intunewinファイルの解凍ができない

公式ツールには解凍機能がありません。

作成した.intunewinファイルの中身を確認したり、トラブルシューティングのために内容を検証したりする手段がないのは不便でした。

これらの課題を解決するため、クロスプラットフォームで動作し、作成と解凍の両方に対応したツールを開発することにしました。

intunewinの使い方

Releasesページから、各プラットフォーム向けのバイナリをダウンロードできます。

フォルダをintunewinファイルにパッケージ化

アプリケーションのファイル群が格納されたフォルダを.intunewinファイルにパッケージ化します。

$ intunewin pack <ソースフォルダ> <出力ファイル.intunewin>

実行例:

$ intunewin pack ./myapp ./dist/myapp.intunewin
Packing ./myapp to ./dist/myapp.intunewin...
Successfully created ./dist/myapp.intunewin

intunewinファイルを解凍

.intunewinファイルを解凍して、元のファイル群を取り出します。

$ intunewin unpack <入力ファイル.intunewin> <出力フォルダ>

実行例:

$ intunewin unpack myapp.intunewin ./extracted
Unpacking myapp.intunewin to ./extracted...
Successfully extracted to ./extracted

Go言語のAPIとして使用

プログラムから利用する場合は、パッケージとしてインポートできます。

package main

import (
    "archive/zip"
    "bytes"
    "fmt"
    "io"
    "os"
    "github.com/kenchan0130/intunewin/pkg/intunewin"
)

func main() {
    // Pack: ZIPアーカイブを作成
    zipBuf := new(bytes.Buffer)
    zipWriter := zip.NewWriter(zipBuf)
    
    // ファイルをZIPに追加
    w1, _ := zipWriter.Create("app.exe")
    w1.Write([]byte("executable content"))
    
    w2, _ := zipWriter.Create("config/settings.json")
    w2.Write([]byte(`{"version": "1.0"}`))
    
    zipWriter.Close()
    
    // ZIPをintunewin形式にパッケージ化
    packedReader, err := intunewin.PackReader(bytes.NewReader(zipBuf.Bytes()), "MyApp", "app.exe")
    if err != nil {
        fmt.Printf("Pack failed: %v\n", err)
        return
    }
    
    // ファイルに書き込み
    packedData, _ := io.ReadAll(packedReader)
    os.WriteFile("myapp.intunewin", packedData, 0644)
    
    // Unpack: intunewinファイルからZIPを抽出
    input, _ := os.ReadFile("myapp.intunewin")
    
    unpackedZipReader, err := intunewin.UnpackReader(bytes.NewReader(input))
    if err != nil {
        fmt.Printf("Unpack failed: %v\n", err)
        return
    }
    
    // ZIPデータを読み取り
    unpackedZipData, _ := io.ReadAll(unpackedZipReader)
    
    // ZIPからファイルを展開
    zipReader, _ := zip.NewReader(bytes.NewReader(unpackedZipData), int64(len(unpackedZipData)))
    for _, file := range zipReader.File {
        fmt.Printf("File: %s\n", file.Name)
    }
}

intunewinファイルの仕様と実装の詳細

ここからは、.intunewinファイルの内部仕様と、その実装方法について解説します。

intunewinファイルの構造

.intunewinファイルは、実体はZIPアーカイブです。

以下の2つのファイルで構成されています。

IntuneWinPackage/
├── Metadata/
│   └── Detection.xml    # 暗号鍵やファイル情報のメタデータ
└── Contents/
    └── IntunePackage.intunewin    # 暗号化されたコンテンツ

Detection.xml

Detection.xmlには、暗号化に使用される鍵情報やファイルのメタデータが含まれます。

<ApplicationInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                 ToolVersion="1.4.0.0">
  <Name>MyApp</Name>
  <UnencryptedContentSize>1234567</UnencryptedContentSize>
  <FileName>IntunePackage.intunewin</FileName>
  <SetupFile>setup.exe</SetupFile>
  <EncryptionInfo>
    <EncryptionKey>Base64エンコードされた256ビットAES鍵</EncryptionKey>
    <MacKey>Base64エンコードされた256ビットHMAC鍵</MacKey>
    <InitializationVector>Base64エンコードされた128ビットIV</InitializationVector>
    <Mac>Base64エンコードされたHMAC値</Mac>
    <ProfileIdentifier>ProfileVersion1</ProfileIdentifier>
    <FileDigest>Base64エンコードされたSHA256ハッシュ</FileDigest>
    <FileDigestAlgorithm>SHA256</FileDigestAlgorithm>
  </EncryptionInfo>
</ApplicationInfo>

主要な項目の説明:

  • EncryptionKey: コンテンツの暗号化に使用される256ビット(32バイト)のAES鍵
  • MacKey: 改ざん検証用のHMAC-SHA256に使用される256ビット(32バイト)の鍵
  • InitializationVector: AES-CBC暗号化の初期化ベクトル(16バイト)
  • Mac: 暗号化データのHMAC-SHA256ハッシュ値(32バイト)
  • FileDigest: 元データ(暗号化前)のSHA256ハッシュ値(32バイト)

IntunePackage.intunewin

IntunePackage.intunewinは、アプリケーションファイル群をZIP圧縮し、AES-256-CBCで暗号化したデータです。

暗号化されたデータの構造は以下の通りです。

[HMAC (32バイト)][IV (16バイト)][暗号化データ]
  • HMAC: IVと暗号化データを結合したもののHMAC-SHA256
  • IV: 初期化ベクトル(Detection.xmlのものと同じ)
  • 暗号化データ: PKCS7パディングを適用したZIPデータをAES-256-CBCで暗号化

暗号化処理の実装

暗号化処理は、以下の手順で行います。

  1. 暗号化鍵、HMAC鍵、初期化ベクトルをランダム生成
  2. 元データのSHA256ハッシュを計算
  3. 元データにPKCS7パディングを適用
  4. AES-256-CBCで暗号化
  5. IVと暗号化データを結合してHMAC-SHA256を計算
  6. [HMAC][IV][暗号化データ]の形式で出力

以下はGo言語での実装例です。

func Encrypt(input io.Reader, output io.Writer, encryptionKey, macKey, iv []byte) ([]byte, error) {
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return nil, fmt.Errorf("failed to create cipher: %w", err)
    }

    // 入力データを全て読み込み
    plaintext, err := io.ReadAll(input)
    if err != nil {
        return nil, fmt.Errorf("failed to read input: %w", err)
    }

    // PKCS7パディングを適用
    plaintext = pkcs7Pad(plaintext, aes.BlockSize)

    // データを暗号化
    mode := cipher.NewCBCEncrypter(block, iv)
    ciphertext := make([]byte, len(plaintext))
    mode.CryptBlocks(ciphertext, plaintext)

    // IV + 暗号化データのHMACを計算
    h := hmac.New(sha256.New, macKey)
    h.Write(iv)
    h.Write(ciphertext)
    mac := h.Sum(nil)

    // [HMAC][IV][暗号化データ]の順で出力
    output.Write(mac)
    output.Write(iv)
    output.Write(ciphertext)

    return mac, nil
}

PKCS7パディングの実装:

func pkcs7Pad(data []byte, blockSize int) []byte {
    padding := blockSize - (len(data) % blockSize)
    padText := make([]byte, padding)
    for i := range padText {
        padText[i] = byte(padding)
    }
    return append(data, padText...)
}

復号化処理の実装

復号処理は、暗号化処理の逆順で行います。

  1. 暗号化データから[HMAC][IV][暗号化データ]を読み取り
  2. IVと暗号化データからHMAC-SHA256を再計算
  3. 保存されているHMACと一致することを確認(改ざん検証)
  4. AES-256-CBCで復号
  5. PKCS7パディングを除去

以下はGo言語での実装例です。

func Decrypt(input io.Reader, output io.Writer, encryptionKey, macKey []byte) error {
    // HMACを読み取り
    storedMac := make([]byte, 32)
    if _, err := io.ReadFull(input, storedMac); err != nil {
        return fmt.Errorf("failed to read HMAC: %w", err)
    }

    // IVを読み取り
    iv := make([]byte, aes.BlockSize)
    if _, err := io.ReadFull(input, iv); err != nil {
        return fmt.Errorf("failed to read IV: %w", err)
    }

    // 暗号化データを全て読み取り
    encryptedData, err := io.ReadAll(input)
    if err != nil {
        return fmt.Errorf("failed to read encrypted data: %w", err)
    }

    // HMACを検証
    h := hmac.New(sha256.New, macKey)
    h.Write(iv)
    h.Write(encryptedData)
    computedMac := h.Sum(nil)

    if !hmac.Equal(storedMac, computedMac) {
        return fmt.Errorf("HMAC verification failed")
    }

    // データを復号化
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return fmt.Errorf("failed to create cipher: %w", err)
    }

    if len(encryptedData)%aes.BlockSize != 0 {
        return fmt.Errorf("encrypted data length is not a multiple of block size")
    }

    mode := cipher.NewCBCDecrypter(block, iv)
    plaintext := make([]byte, len(encryptedData))
    mode.CryptBlocks(plaintext, encryptedData)

    // PKCS7パディングを除去
    plaintext, err = pkcs7Unpad(plaintext, aes.BlockSize)
    if err != nil {
        return fmt.Errorf("failed to remove padding: %w", err)
    }

    output.Write(plaintext)
    return nil
}

パッケージ化処理の全体フロー

最後に、フォルダを.intunewinファイルにパッケージ化する全体の流れを説明します。

  1. ソースフォルダのファイルをZIPアーカイブに圧縮
  2. ZIPデータのSHA256ハッシュを計算
  3. 暗号化鍵、HMAC鍵、初期化ベクトルをランダム生成
  4. ZIPデータを暗号化
  5. メタデータXMLを作成
  6. 最終的な.intunewinファイル(ZIPアーカイブ)を生成
    • IntuneWinPackage/Metadata/Detection.xmlにメタデータを配置
    • IntuneWinPackage/Contents/IntunePackage.intunewinに暗号化データを配置

参考実装

このツールの実装にあたっては、simeoncloud/IntuneAppBuilderを参考にしました。

IntuneAppBuilderは.NET実装のツールで、.intunewinファイルの仕様を理解するのに非常に役立ちました。

まとめ

クロスプラットフォームで動作するintunewinファイルの作成・解凍ツールを開発しました。

このツールにより、以下のことが可能になりました。

  • Windows、macOS、Linuxで.intunewinファイルを作成できる
  • .intunewinファイルの中身を確認できる
  • CI/CDパイプラインに組込みやすい
  • Go言語のライブラリとしてプログラムから利用できる

まだ作りたてであるため、不具合などがあるかもしれません。 お気軽にGitHubのIssueにてお知らせください。

皆様からのフィードバックをお待ちしています。