InfrastructureBackupValidator/Unprotect-AzsBackup.psm1 (246 lines of code) (raw):
#------------------------------------------------------------------
# <copyright file="Unprotect-AzsBackup.psm1" company="Microsoft Corp.">
# Copyright (c) Microsoft Corp. All rights reserved.
# </copyright>
#------------------------------------------------------------------
#Requires -Version 5
function Unprotect-AzsBackupFile {
param (
[string]
$SourcePath,
[string]
$DestinationPath,
[byte[]]
$EncKey,
[byte[]]
$MacKey
)
$ErrorActionPreference = "Stop"
Write-Verbose "[$($MyInvocation.MyCommand)] Decryption started: Source: $SourcePath, Destinantion $DestinationPath"
$source = New-Object System.IO.FileStream -ArgumentList @($SourcePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read)
try {
# Read header
$null = $source.Seek(0, [System.IO.SeekOrigin]::Begin)
$header = Read-SymmetricEncryptionHeader -Stream $source
# Check auth tag
$null = $source.Seek($header["headerLength"], [System.IO.SeekOrigin]::Begin)
$authTag = Get-AuthTag -Stream $source -Key $MacKey -Algorithm $header["AuthAlgType"]
if ($null -ne $(Compare-Object $authTag $header["authTag"])) {
throw "Encrypted data checksum mismatch."
}
Write-Verbose "[$($MyInvocation.MyCommand)] Auth tag check passed"
# Get decryptor
$decryptor = Initialize-SymmetricDecryptor -Key $EncKey -IvBytes $header["IvBytes"] -Algorithm $header["EncAlgType"]
# Start decryption
$null = $source.Seek($header["headerLength"], [System.IO.SeekOrigin]::Begin)
$destination = New-Object System.IO.FileStream -ArgumentList @($DestinationPath, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write)
try {
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream -ArgumentList @($source, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)
Write-Verbose "[$($MyInvocation.MyCommand)] Decryption started"
$null = $cryptoStream.CopyToAsync($destination).GetAwaiter().GetResult()
Write-Verbose "[$($MyInvocation.MyCommand)] Decryption completed"
} catch {
throw $_
} finally {
$null = $destination.Flush($true);
$null = $destination.Dispose()
}
} catch {
throw $_
} finally {
$null = $source.Dispose()
}
Write-Verbose "[$($MyInvocation.MyCommand)] Decryption finished: Source: $SourcePath, Destinantion $DestinationPath"
}
function Unprotect-AzsBackupWrappedKey {
param(
[string]
$WrappedKey,
[System.Security.Cryptography.X509Certificates.X509Certificate2]
$Cert
)
$ErrorActionPreference = "Stop"
# first 32 byte (256 bits) of the binary is encryption key, the rest is MAC (Message Authentication Code) key.
# In Azurestack, we use 256 bits mac key.
$encKeyLength = 32
$macKeyLength = 32
[System.Security.Cryptography.RSA] $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Cert)
$bytesEncrypted = [System.Convert]::FromBase64String($WrappedKey)
try {
$bytesDecrypted = $rsa.Decrypt($bytesEncrypted, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
}
catch {
# Retry with OaepSHA1 if decryption with OaepSHA256 fails
$bytesDecrypted = $rsa.Decrypt($bytesEncrypted, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)
}
[string] $UnwrappedKey = [System.Text.Encoding]::UTF8.GetString($bytesDecrypted)
$bytesCompactKey = [System.Convert]::FromBase64String($UnwrappedKey)
$macKeyLength = $($bytesCompactKey.Length - $encKeyLength)
$encKey = New-Object byte[] $encKeyLength
$macKey = New-Object byte[] $($bytesCompactKey.Length - $encKeyLength)
Write-Verbose "[$($MyInvocation.MyCommand)] EncKey length: $($encKey.Length * 8) bits, MacKey length: $($macKey.Length * 8) bits"
if ($macKey.Length -ne $macKeyLength) {
Write-Warning "[$($MyInvocation.MyCommand)] Mackey should be 256 bits long"
}
[System.Buffer]::BlockCopy($bytesCompactKey, 0, $encKey, 0, $encKeyLength)
[System.Buffer]::BlockCopy($bytesCompactKey, $encKeyLength, $macKey, 0, $macKeyLength)
return $encKey, $macKey
}
function Read-SymmetricEncryptionHeader {
param(
[System.IO.FileStream]
$Stream
)
$ErrorActionPreference = "Stop"
$SymmetricEncryptionHeaderMagicNumber = 1
$reader = New-Object System.IO.BinaryReader -ArgumentList $Stream
# header tag or version
[int16] $headerTag = $reader.ReadInt16()
#
if ($headerTag -ne $SymmetricEncryptionHeaderMagicNumber)
{
throw "Encrypted data is corrupted or not supported."
}
# encryption algorithm type
[int16] $EncAlgType = $reader.ReadInt16();
# authentication algorithm type
[int16] $AuthAlgType = $reader.ReadInt16();
# IV length
[int16] $IVLength = $reader.ReadInt16();
# authentication tag length
[int16] $authTagLength = $reader.ReadInt16();
# Initialization vector (IV)
[byte[]] $IvBytes = $reader.ReadBytes($IVLength);
# authentication tag content.
[byte[]]$authTag = $reader.ReadBytes($authTagLength);
# headerLength = 5 * size of Int16 + size of IV + size of auth tag
[int16] $headerLength = 5 * 2 + $IVLength + $authTagLength
return @{
"EncAlgType" = $EncAlgType
"AuthAlgType" = $AuthAlgType
"IvBytes" = $IvBytes
"authTag" = $authTag
"headerLength" = $headerLength
}
}
function Initialize-SymmetricDecryptor {
param(
[byte[]]
$Key,
[Byte[]]
$IvBytes,
[int16]
$Algorithm
)
$ErrorActionPreference = "Stop"
if ($Algorithm -ne 0){
throw "Unsupported encryption algorithm: $Algorithm"
}
Write-Verbose "[$($MyInvocation.MyCommand)] Encryption algorithm: AES-256 (CBC) with PKCS7 padding"
$alg = New-Object System.Security.Cryptography.AesCryptoServiceProvider -Property @{
"Mode" = [System.Security.Cryptography.CipherMode]::CBC
"Padding" = [System.Security.Cryptography.PaddingMode]::PKCS7
}
return $alg.CreateDecryptor($Key, $IvBytes)
}
function Get-AuthTag {
param(
[System.IO.FileStream]
$Stream,
[byte[]]
$Key,
[int16]
$Algorithm
)
$ErrorActionPreference = "Stop"
if ($Algorithm -ne 0){
throw "Unsupported authentication algorithm: $Algorithm"
}
Write-Verbose "[$($MyInvocation.MyCommand)] Authentication algorithm: HMAC-SHA512 with key length $($Key.Length * 8) bits"
$alg = [System.Security.Cryptography.HMAC]::Create("HMACSHA512")
$alg.Key = $Key
return $alg.ComputeHash($stream)
}
function Unprotect-AzsBackup {
<#
.SYNOPSIS
Decrypt Azurestack infrastructure backup
.PARAMETER BackupSnapshotZip
The zip file of the backup snapshot
.PARAMETER Destination
The destinantion of decrypted backup file
.PARAMETER Certificate
The .pfx file contains private key to decrypt backup
.PARAMETER CertificatePassphrase
The Passphrase for certificate
.PARAMETER ShareCrendential
The crendential for backup share
#>
[cmdletbinding()]
param(
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string] $BackupSnapshotZip,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string] $Destination,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string] $Certificate,
[Parameter(Mandatory=$false)]
[SecureString] $CertificatePassphrase = $null,
[Parameter(Mandatory=$false)]
[PSCredential] $ShareCrendential = $null
)
$ErrorActionPreference = "Stop"
if ($null -eq $CertificatePassphrase)
{
$CertificatePassphrase = Read-Host -Prompt "Enter password" -AsSecureString
}
Write-Verbose "[$($MyInvocation.MyCommand)] Mapping remote fileshare to PSdrive"
$BackupPath = [System.IO.Path]::GetDirectoryName($BackupSnapshotZip)
Write-Verbose "[$($MyInvocation.MyCommand)] Loading certificate"
try
{
$certBytes = [System.IO.File]::ReadAllBytes($Certificate)
}
catch
{
throw "Unable to read certificate file: $_"
}
$rawPassphrase = $(New-Object PSCredential " ",$CertificatePassphrase).GetNetworkCredential().Password
try
{
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($certBytes, $rawPassphrase)
}
catch [System.Management.Automation.MethodInvocationException]
{
throw "[$($MyInvocation.MyCommand)] Unable to load certificate. Passphrase is incorrect or the certificate is corrupted."
}
Write-Verbose "[$($MyInvocation.MyCommand)] Cert info: $cert"
$snapshotMetadata = $BackupSnapshotZip.Replace(".zip", ".xml")
if (!(Test-Path $BackupSnapshotZip) -or !(Test-Path $snapshotMetadata))
{
throw "Can not find valid backup"
}
[xml]$snapshot = Get-Content $snapshotMetadata
$wrappedKey = $(Select-Xml -Xml $snapshot -XPath "/BackupSnapshot/EncryptedBackupEncryptionKey").Node.InnerText
$certThumprint = $(Select-Xml -Xml $snapshot -XPath "/BackupSnapshot/EncryptionCertThumbprint").Node.InnerText
$backupStatus = $(Select-Xml -Xml $snapshot -XPath "/BackupSnapshot/SnapshotProperties/BackupStatus").Node.InnerText
if ($backupStatus -ne "Succeeded")
{
Write-Warning "[$($MyInvocation.MyCommand)] Backup file status is $backupStatus."
}
Write-Verbose "[$($MyInvocation.MyCommand)] Wrapped key : $wrappedKey"
if ($cert.Thumbprint -ne $certThumprint)
{
throw "Decryption key is not the same as encryption key: Thumbprint mismatch"
}
$keys = Unprotect-AzsBackupWrappedKey -WrappedKey $wrappedKey -Cert $cert
$zip = Get-ChildItem $BackupSnapshotZip
$destinationFilePath = Join-Path $Destination $zip.Name
Unprotect-AzsBackupFile -SourcePath $BackupSnapshotZip -DestinationPath $destinationFilePath -EncKey $keys[0] -MacKey $keys[1]
Expand-Archive -Path $destinationFilePath -DestinationPath $Destination -Force
Remove-Item -Force -Confirm:$false $destinationFilePath -ErrorAction SilentlyContinue
}
Export-ModuleMember -Function Unprotect-AzsBackup