Public/Tests/Unprotect-ConfigValue.Tests.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
    Justification = 'Builds a value in the legacy 0.2.x format to test backward compatibility.')]
param()

BeforeAll {
    $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config'
    $env:TCS_SKIP_UPDATE_CHECK = '1'
    $env:TCS_TELEMETRY_OPTOUT = '1'
    $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent
    Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.core.psd1') -Force
}

AfterAll {
    Remove-Module -Name tcs.core -Force -ErrorAction SilentlyContinue
}

Describe 'Unprotect-ConfigValue' {
    It "Should decrypt to original value" {
        $original = "RoundTripTestValue"
        $encrypted = Protect-ConfigValue -Value $original
        $decrypted = Unprotect-ConfigValue -EncryptedValue $encrypted
        $decrypted | Should -Be $original
    }

    It "Should return a string" {
        $encrypted = Protect-ConfigValue -Value "TypeCheckValue"
        $result = Unprotect-ConfigValue -EncryptedValue $encrypted
        $result | Should -BeOfType [string]
    }
}

Describe 'Unprotect-ConfigValue format and platforms' {
    BeforeAll {
        $key = New-Object byte[] 32
        [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($key)
    }

    It 'Round-trips unicode text' {
        $text = 'pässwörd-✓-日本'
        Unprotect-ConfigValue -EncryptedValue (Protect-ConfigValue -Value $text) | Should -BeExactly $text
    }

    It 'Round-trips with a supplied key' {
        Unprotect-ConfigValue -EncryptedValue (Protect-ConfigValue -Value 'k' -Key $key) -Key $key | Should -BeExactly 'k'
    }

    It 'Requires the key for key-protected values' {
        $protected = Protect-ConfigValue -Value 'k' -Key $key
        Unprotect-ConfigValue -EncryptedValue $protected -ErrorAction SilentlyContinue -ErrorVariable errs | Should -BeNullOrEmpty
        $errs[0].ToString() | Should -Match '-Key'
    }

    It 'Fails with the wrong key' {
        $other = New-Object byte[] 32
        $protected = Protect-ConfigValue -Value 'k' -Key $key
        { Unprotect-ConfigValue -EncryptedValue $protected -Key $other -ErrorAction Stop } | Should -Throw
    }

    It 'Detects tampering' {
        $protected = Protect-ConfigValue -Value 'k' -Key $key
        $bytes = [Convert]::FromBase64String($protected.Split(':')[3])
        $bytes[20] = $bytes[20] -bxor 1
        $tampered = 'tcs:v1:aes-key:' + [Convert]::ToBase64String($bytes)
        { Unprotect-ConfigValue -EncryptedValue $tampered -Key $key -ErrorAction Stop } | Should -Throw
    }

    It 'Returns a SecureString with -AsSecureString' {
        $secure = Unprotect-ConfigValue -EncryptedValue (Protect-ConfigValue -Value 's') -AsSecureString
        $secure | Should -BeOfType [System.Security.SecureString]
        [System.Net.NetworkCredential]::new('', $secure).Password | Should -BeExactly 's'
    }

    It 'Still reads values written by tcs.core 0.2.x, with a warning' {
        $legacyKey = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($env:COMPUTERNAME + 'tcs.core'))
        $legacy = ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String 'old' -AsPlainText -Force) -Key $legacyKey
        Unprotect-ConfigValue -EncryptedValue $legacy -Scope LocalMachine -WarningVariable warnings -WarningAction SilentlyContinue | Should -BeExactly 'old'
        $warnings.Count | Should -Be 1
    }

    It 'Gives a clear error, and no legacy warning, for a value that was never protected' -ForEach @(
        @{ Value = 'hello' }, @{ Value = 'tcs:v2:x' }, @{ Value = 'abc' }
    ) {
        $warnings = $null
        $errs = $null
        Unprotect-ConfigValue -EncryptedValue $Value -WarningVariable warnings -WarningAction SilentlyContinue -ErrorVariable errs -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
        $warnings.Count | Should -Be 0
        $errs[0].ToString() | Should -Match 'not protected'
    }

    It 'Reads a legacy LocalMachine value whatever -Scope is given' {
        $legacyKey = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes([string]$env:COMPUTERNAME + 'tcs.core'))
        $legacy = ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String 'old' -AsPlainText -Force) -Key $legacyKey
        Unprotect-ConfigValue -EncryptedValue $legacy -WarningAction SilentlyContinue | Should -BeExactly 'old'
    }

    It 'Reads a legacy LocalMachine value keyed with the machine name' {
        $legacyKey = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes([Environment]::MachineName + 'tcs.core'))
        $legacy = ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String 'machine' -AsPlainText -Force) -Key $legacyKey
        Unprotect-ConfigValue -EncryptedValue $legacy -Scope LocalMachine -WarningAction SilentlyContinue | Should -BeExactly 'machine'
    }

    It 'Reads a legacy LocalMachine value written where COMPUTERNAME was empty' {
        $legacyKey = [System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes('tcs.core'))
        $legacy = ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String 'empty' -AsPlainText -Force) -Key $legacyKey
        Unprotect-ConfigValue -EncryptedValue $legacy -Scope LocalMachine -WarningAction SilentlyContinue | Should -BeExactly 'empty'
    }

    It 'Reads a legacy CurrentUser value' {
        $legacy = ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String 'user value' -AsPlainText -Force)
        Unprotect-ConfigValue -EncryptedValue $legacy -WarningAction SilentlyContinue | Should -BeExactly 'user value'
    }
}