Private/WinGet/New-IntuneWinPackage.ps1
|
function New-IntuneWinPackage { <# .SYNOPSIS Builds a repo-owned .intunewin package. .DESCRIPTION Packages a source directory into the Intune portal-compatible .intunewin structure by zipping the source, encrypting the inner archive, generating a Detection.xml manifest, and producing the final outer package. .PARAMETER PackagingContext Packaging context returned by New-IntuneWinPackagingContext. .OUTPUTS PSCustomObject describing the generated package. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ValidateNotNull()] [psobject]$PackagingContext ) function ConvertTo-Base64 { param( [Parameter(Mandatory)] [byte[]]$Value ) return [Convert]::ToBase64String($Value) } function Format-DetectionXml { param( [Parameter(Mandatory)] [string]$DisplayName, [Parameter(Mandatory)] [string]$InnerFileName, [Parameter(Mandatory)] [string]$SetupFile, [Parameter(Mandatory)] [int64]$UnencryptedContentSize, [Parameter(Mandatory)] [hashtable]$EncryptionInfo ) $xml = [System.Xml.XmlDocument]::new() $applicationInfo = $xml.CreateElement('ApplicationInfo') $applicationInfo.SetAttribute('ToolVersion', '1.4.0.0') [void]$xml.AppendChild($applicationInfo) foreach ($node in @( @{ Name = 'Name'; Value = $DisplayName }, @{ Name = 'UnencryptedContentSize'; Value = $UnencryptedContentSize }, @{ Name = 'FileName'; Value = $InnerFileName }, @{ Name = 'SetupFile'; Value = $SetupFile } )) { $element = $xml.CreateElement($node.Name) $element.InnerText = [string]$node.Value [void]$applicationInfo.AppendChild($element) } $encryptionElement = $xml.CreateElement('EncryptionInfo') foreach ($node in @( @{ Name = 'EncryptionKey'; Value = $EncryptionInfo.EncryptionKey }, @{ Name = 'MacKey'; Value = $EncryptionInfo.MacKey }, @{ Name = 'InitializationVector'; Value = $EncryptionInfo.InitializationVector }, @{ Name = 'Mac'; Value = $EncryptionInfo.Mac }, @{ Name = 'ProfileIdentifier'; Value = $EncryptionInfo.ProfileIdentifier }, @{ Name = 'FileDigest'; Value = $EncryptionInfo.FileDigest }, @{ Name = 'FileDigestAlgorithm'; Value = $EncryptionInfo.FileDigestAlgorithm } )) { $element = $xml.CreateElement($node.Name) $element.InnerText = [string]$node.Value [void]$encryptionElement.AppendChild($element) } [void]$applicationInfo.AppendChild($encryptionElement) $builder = [System.Text.StringBuilder]::new() $settings = [System.Xml.XmlWriterSettings]::new() $settings.Indent = $true $settings.IndentChars = ' ' $settings.NewLineChars = "`r`n" $settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace $settings.OmitXmlDeclaration = $true $writer = [System.Xml.XmlWriter]::Create($builder, $settings) try { $xml.Save($writer) } finally { $writer.Dispose() } return $builder.ToString() } function Protect-ArchiveFile { param( [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory)] [string]$DestinationPath ) $encryptionAlgorithm = [System.Security.Cryptography.Aes]::Create() $macAlgorithm = [System.Security.Cryptography.Aes]::Create() $hashAlgorithm = [System.Security.Cryptography.SHA256]::Create() $macLength = 32 $sourceHashStream = $null $outputStream = $null $sourceStream = $null $cryptoStream = $null $hmac = $null try { $encryptionAlgorithm.GenerateKey() $macAlgorithm.GenerateKey() $initializationVector = $encryptionAlgorithm.IV $sourceHashStream = [System.IO.File]::OpenRead($SourcePath) $fileDigest = $hashAlgorithm.ComputeHash($sourceHashStream) $sourceHashStream.Dispose() $sourceHashStream = $null $outputStream = [System.IO.File]::Open($DestinationPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) $placeholderBytes = [byte[]]::new($macLength + $initializationVector.Length) $outputStream.Write($placeholderBytes, 0, $placeholderBytes.Length) $sourceStream = [System.IO.File]::OpenRead($SourcePath) $encryptor = $encryptionAlgorithm.CreateEncryptor($encryptionAlgorithm.Key, $initializationVector) $cryptoStream = [System.Security.Cryptography.CryptoStream]::new($outputStream, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write, $true) $sourceStream.CopyTo($cryptoStream) $cryptoStream.FlushFinalBlock() $cryptoStream.Dispose() $cryptoStream = $null $sourceStream.Dispose() $sourceStream = $null $outputStream.Position = $macLength $outputStream.Write($initializationVector, 0, $initializationVector.Length) $outputStream.Flush() $hmac = [System.Security.Cryptography.HMACSHA256]::new($macAlgorithm.Key) $outputStream.Position = $macLength $mac = $hmac.ComputeHash($outputStream) $hmac.Dispose() $hmac = $null $outputStream.Position = 0 $outputStream.Write($mac, 0, $mac.Length) $outputStream.Flush() return @{ EncryptionKey = ConvertTo-Base64 -Value $encryptionAlgorithm.Key MacKey = ConvertTo-Base64 -Value $macAlgorithm.Key InitializationVector = ConvertTo-Base64 -Value $initializationVector Mac = ConvertTo-Base64 -Value $mac ProfileIdentifier = 'ProfileVersion1' FileDigest = ConvertTo-Base64 -Value $fileDigest FileDigestAlgorithm = 'SHA256' } } finally { if ($null -ne $hmac) { $hmac.Dispose() } if ($null -ne $cryptoStream) { $cryptoStream.Dispose() } if ($null -ne $sourceStream) { $sourceStream.Dispose() } if ($null -ne $sourceHashStream) { $sourceHashStream.Dispose() } if ($null -ne $outputStream) { $outputStream.Dispose() } $encryptionAlgorithm.Dispose() $macAlgorithm.Dispose() $hashAlgorithm.Dispose() } } function Add-ZipEntryFromFile { param( [Parameter(Mandatory)] [System.IO.Compression.ZipArchive]$Archive, [Parameter(Mandatory)] [string]$EntryName, [Parameter(Mandatory)] [string]$SourcePath ) $entry = $Archive.CreateEntry($EntryName, [System.IO.Compression.CompressionLevel]::NoCompression) $entryStream = $entry.Open() try { $sourceStream = [System.IO.File]::OpenRead($SourcePath) try { $sourceStream.CopyTo($entryStream) } finally { $sourceStream.Dispose() } } finally { $entryStream.Dispose() } } function Add-ZipEntryFromText { param( [Parameter(Mandatory)] [System.IO.Compression.ZipArchive]$Archive, [Parameter(Mandatory)] [string]$EntryName, [Parameter(Mandatory)] [string]$Content ) $entry = $Archive.CreateEntry($EntryName, [System.IO.Compression.CompressionLevel]::NoCompression) $entryStream = $entry.Open() $writer = [System.IO.StreamWriter]::new($entryStream, [System.Text.Encoding]::UTF8) try { $writer.Write($Content) } finally { $writer.Dispose() $entryStream.Dispose() } } Add-Type -AssemblyName System.IO.Compression.FileSystem if (-not $PackagingContext.PackagingCapability -or -not $PackagingContext.PackagingCapability.SupportsCrossPlatformIntuneWin) { throw 'The current platform does not support repo-owned .intunewin packaging.' } if (-not (Test-Path -Path $PackagingContext.SourcePath -PathType Container)) { throw "Packaging source path not found: $($PackagingContext.SourcePath)" } if ([string]::IsNullOrWhiteSpace($PackagingContext.SetupFile)) { throw 'PackagingContext.SetupFile is required.' } if ([string]::IsNullOrWhiteSpace($PackagingContext.OutputPath)) { throw 'PackagingContext.OutputPath is required.' } $outputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PackagingContext.OutputPath) $outputDirectory = Split-Path -Path $outputPath -Parent if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -Path $outputDirectory)) { $null = New-Item -Path $outputDirectory -ItemType Directory -Force -ErrorAction Stop } $workingRoot = Join-Path -Path $outputDirectory -ChildPath ('.winget-packaging-{0}' -f [guid]::NewGuid().ToString('N')) $null = New-Item -Path $workingRoot -ItemType Directory -Force -ErrorAction Stop $sourceArchivePath = Join-Path -Path $workingRoot -ChildPath 'source-content.zip' $encryptedArchivePath = Join-Path -Path $workingRoot -ChildPath 'IntunePackage.intunewin' $innerFileName = 'IntunePackage.intunewin' Write-Debug "Creating .intunewin package for '$($PackagingContext.PackageIdentifier)' version '$($PackagingContext.PackageVersion)'. SourcePath='$($PackagingContext.SourcePath)', SetupFile='$($PackagingContext.SetupFile)', OutputPath='$($PackagingContext.OutputPath)', WorkingRoot='$workingRoot'." try { if (Test-Path -Path $outputPath) { if (-not $PSCmdlet.ShouldProcess($outputPath, 'Overwrite existing .intunewin package')) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.OperationCanceledException]::new("Refused to overwrite existing package '$($PackagingContext.OutputPath)'."), 'IntuneWinPackageOverwriteDeclined', [System.Management.Automation.ErrorCategory]::OperationStopped, $outputPath ) $PSCmdlet.ThrowTerminatingError($errorRecord) } Write-Debug "Removing existing .intunewin output at '$($PackagingContext.OutputPath)'." Remove-Item -Path $outputPath -Force -ErrorAction Stop } [System.IO.Compression.ZipFile]::CreateFromDirectory( $PackagingContext.SourcePath, $sourceArchivePath, [System.IO.Compression.CompressionLevel]::Optimal, $false ) $unencryptedContentSize = (Get-Item -Path $sourceArchivePath).Length Write-Debug "Created source archive '$sourceArchivePath' ($unencryptedContentSize bytes)." $encryptionInfo = Protect-ArchiveFile -SourcePath $sourceArchivePath -DestinationPath $encryptedArchivePath $encryptedContentSize = (Get-Item -Path $encryptedArchivePath).Length Write-Debug "Encrypted WinGet package payload to '$encryptedArchivePath' ($encryptedContentSize bytes)." $displayName = if ($PackagingContext.DisplayName) { $PackagingContext.DisplayName } elseif ($PackagingContext.PackageIdentifier) { $PackagingContext.PackageIdentifier } else { $PackagingContext.SetupFile } $detectionXml = Format-DetectionXml -DisplayName $displayName -InnerFileName $innerFileName -SetupFile $PackagingContext.SetupFile -UnencryptedContentSize $unencryptedContentSize -EncryptionInfo $encryptionInfo $fileStream = [System.IO.File]::Open($outputPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) try { $archive = [System.IO.Compression.ZipArchive]::new($fileStream, [System.IO.Compression.ZipArchiveMode]::Create, $false) try { Add-ZipEntryFromFile -Archive $archive -EntryName 'IntuneWinPackage/Contents/IntunePackage.intunewin' -SourcePath $encryptedArchivePath Add-ZipEntryFromText -Archive $archive -EntryName 'IntuneWinPackage/Metadata/Detection.xml' -Content $detectionXml } finally { $archive.Dispose() } } finally { $fileStream.Dispose() } $outputSize = (Get-Item -Path $outputPath).Length Write-Debug "Created final .intunewin package at '$($PackagingContext.OutputPath)' ($outputSize bytes)." return [PSCustomObject]@{ OutputPath = $PackagingContext.OutputPath FileName = [System.IO.Path]::GetFileName($PackagingContext.OutputPath) SetupFile = $PackagingContext.SetupFile UnencryptedSize = $unencryptedContentSize EncryptionInfo = [PSCustomObject]$encryptionInfo PackagingContext = $PackagingContext } } finally { if (Test-Path -Path $workingRoot) { Write-Debug "Removing packaging working directory '$workingRoot'." try { Remove-Item -Path $workingRoot -Recurse -Force -ErrorAction Stop } catch { Write-Debug "Failed to remove packaging working directory '$workingRoot'. Error='$($_.Exception.Message)'." } } } } |