Private/Protect-MachineBoundString.ps1
|
Function Protect-MachineBoundString { <# Wraps an already key-encrypted string (output of ConvertFrom-SecureString -Key) into a machine-bound outer layer, so a stolen ciphertext + keyfile cannot be decrypted on another machine. Windows (Desktop + Core): DPAPI (ProtectedData) with the keyfile bytes as additional entropy. Scope 'Machine' -> DataProtectionScope::LocalMachine (any account on this machine that also has the keyfile can decrypt) Scope 'User' -> DataProtectionScope::CurrentUser (bound to this machine AND this user account) Linux/macOS: No DPAPI available. Encryption keys are derived via HMAC-SHA256 from the keyfile bytes and the machine id (plus the username for scope 'User'), then AES-256-CBC with a random IV and HMAC-SHA256 over IV+ciphertext (encrypt-then-MAC). Note: this binding is best effort - an attacker who can read the keyfile AND the machine id can reconstruct the key offline. Output format: ApSec2|<Scope>|<Base64> #> param( [Parameter(Mandatory=$true)][String]$String ,[Parameter(Mandatory=$true)][Byte[]]$KeyBytes ,[Parameter(Mandatory=$true)][ValidateSet('Machine','User')][String]$Scope ) $plainBytes = [System.Text.Encoding]::UTF8.GetBytes($String) If ( $PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows ) { # ProtectedData lives in System.Security on Windows PowerShell 5.1 If ( $PSVersionTable.PSEdition -eq 'Desktop' ) { Add-Type -AssemblyName System.Security } $dpapiScope = If ( $Scope -eq 'User' ) { [System.Security.Cryptography.DataProtectionScope]::CurrentUser } else { [System.Security.Cryptography.DataProtectionScope]::LocalMachine } $protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect($plainBytes, $KeyBytes, $dpapiScope) } else { # Derive independent encryption and MAC keys from keyfile + machine context $context = Get-MachineIdentifier If ( $Scope -eq 'User' ) { $context = "$( $context )|$( [System.Environment]::UserName )" } $kdf = [System.Security.Cryptography.HMACSHA256]::new($KeyBytes) $encKey = $kdf.ComputeHash([System.Text.Encoding]::UTF8.GetBytes("enc|$( $context )")) $macKey = $kdf.ComputeHash([System.Text.Encoding]::UTF8.GetBytes("mac|$( $context )")) $kdf.Dispose() $aes = [System.Security.Cryptography.Aes]::Create() Try { $aes.Key = $encKey $aes.GenerateIV() $encryptor = $aes.CreateEncryptor() $cipherBytes = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length) $encryptor.Dispose() $ivAndCipher = $aes.IV + $cipherBytes } Finally { $aes.Dispose() } $hmac = [System.Security.Cryptography.HMACSHA256]::new($macKey) $mac = $hmac.ComputeHash($ivAndCipher) $hmac.Dispose() $protectedBytes = $ivAndCipher + $mac } "ApSec2|$( $Scope )|$( [Convert]::ToBase64String($protectedBytes) )" } |