Tests/Private/Crypto.Tests.ps1
|
BeforeDiscovery { Import-Module (Join-Path $PSScriptRoot '../../Posh-SecretSharing.psd1') -Force } Describe 'Private/Crypto' -Tag Unit { InModuleScope 'Posh-SecretSharing' { Describe 'Get-SecretSharingCipherConstant' { It 'returns the SLIP-0039 cipher constants' { $constant = Get-SecretSharingCipherConstant $constant.BaseIterationCount | Should -Be 10000 $constant.RoundCount | Should -Be 4 $constant.IdentifierLengthByte | Should -Be 2 $constant.MinimumSecretLength | Should -Be 16 $customization = $constant.CustomizationString $customization | Should -Be ([System.Text.Encoding]::ASCII.GetBytes('shamir')) } } Describe 'Get-SecretSharingRandomIdentifier' { It 'returns a value within the 15-bit range (0-32767)' { for ($i = 0; $i -lt 100; $i++) { $identifier = Get-SecretSharingRandomIdentifier $identifier | Should -BeGreaterOrEqual 0 $identifier | Should -BeLessOrEqual 32767 } } It 'produces different values across repeated calls' { $values = 1..20 | ForEach-Object { Get-SecretSharingRandomIdentifier } ($values | Select-Object -Unique).Count | Should -BeGreaterThan 1 } } Describe 'Get-SecretSharingCipherSaltPrefix' { It 'returns an empty prefix when Extendable is set' { (Get-SecretSharingCipherSaltPrefix -Identifier 1234 -Extendable).Count | Should -Be 0 } It 'returns "shamir" + the identifier as 2 big-endian bytes when not extendable' { $actual = Get-SecretSharingCipherSaltPrefix -Identifier 0x1234 $expected = [byte[]]([System.Text.Encoding]::ASCII.GetBytes('shamir') + [byte[]](0x12, 0x34)) $actual | Should -Be $expected } It 'encodes identifier 0 and the maximum identifier 32767 correctly' { $zero = Get-SecretSharingCipherSaltPrefix -Identifier 0 $zero | Should -Be ([byte[]]([System.Text.Encoding]::ASCII.GetBytes('shamir') + [byte[]](0x00, 0x00))) $max = Get-SecretSharingCipherSaltPrefix -Identifier 32767 $max | Should -Be ([byte[]]([System.Text.Encoding]::ASCII.GetBytes('shamir') + [byte[]](0x7F, 0xFF))) } } Describe 'Invoke-SecretSharingCipherRoundFunction' { It 'returns exactly R.Length bytes' { $r = [byte[]](1, 2, 3, 4, 5, 6, 7, 8) $f = Invoke-SecretSharingCipherRoundFunction -RoundIndex 0 -Passphrase @() -SaltPrefix ([byte[]]@()) -R $r -IterationExponent 0 $f.Count | Should -Be $r.Count } It 'is deterministic for identical inputs' { $r = [byte[]](9, 9, 9, 9, 9, 9, 9, 9) $passphrase = [System.Text.Encoding]::UTF8.GetBytes('correct horse battery staple') $a = Invoke-SecretSharingCipherRoundFunction -RoundIndex 2 -Passphrase $passphrase -SaltPrefix ([byte[]](1, 2)) -R $r -IterationExponent 0 $b = Invoke-SecretSharingCipherRoundFunction -RoundIndex 2 -Passphrase $passphrase -SaltPrefix ([byte[]](1, 2)) -R $r -IterationExponent 0 $a | Should -Be $b } It 'produces a different result for a different RoundIndex' { $r = [byte[]](5, 5, 5, 5, 5, 5, 5, 5) $a = Invoke-SecretSharingCipherRoundFunction -RoundIndex 0 -Passphrase @() -SaltPrefix ([byte[]]@()) -R $r -IterationExponent 0 $b = Invoke-SecretSharingCipherRoundFunction -RoundIndex 1 -Passphrase @() -SaltPrefix ([byte[]]@()) -R $r -IterationExponent 0 ($a -join ',') | Should -Not -Be ($b -join ',') } It 'matches an independently constructed PBKDF2-HMAC-SHA256 call for the same password/salt/iterations' { $roundIndex = [byte]1 $passphrase = [System.Text.Encoding]::UTF8.GetBytes('hunter2') $saltPrefix = [byte[]]([System.Text.Encoding]::ASCII.GetBytes('shamir') + [byte[]](0x00, 0x2A)) $r = [byte[]](11, 22, 33, 44, 55, 66, 77, 88) $iterationExponent = 1 $actual = Invoke-SecretSharingCipherRoundFunction -RoundIndex $roundIndex -Passphrase $passphrase -SaltPrefix $saltPrefix -R $r -IterationExponent $iterationExponent $expectedPassword = [byte[]](@($roundIndex) + $passphrase) $expectedSalt = [byte[]]($saltPrefix + $r) $expectedIterations = [int]((10000 -shl $iterationExponent) / 4) $reference = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($expectedPassword, $expectedSalt, $expectedIterations, [System.Security.Cryptography.HashAlgorithmName]::SHA256) try { $expected = $reference.GetBytes($r.Length) } finally { $reference.Dispose() } $actual | Should -Be $expected } } Describe 'Protect-SecretSharingMasterSecret / Unprotect-SecretSharingMasterSecret' { It 'round-trips: decrypting with the same parameters recovers the original secret' { $secret = Get-SecretSharingRandomByte -Length 16 $passphrase = [System.Text.Encoding]::UTF8.GetBytes('TREZOR') $encrypted = Protect-SecretSharingMasterSecret -MasterSecret $secret -Passphrase $passphrase -Identifier 12345 -IterationExponent 0 $decrypted = Unprotect-SecretSharingMasterSecret -EncryptedMasterSecret $encrypted -Passphrase $passphrase -Identifier 12345 -IterationExponent 0 $decrypted | Should -Be $secret } It 'round-trips with an empty passphrase' { $secret = Get-SecretSharingRandomByte -Length 32 $encrypted = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 1 -IterationExponent 0 $decrypted = Unprotect-SecretSharingMasterSecret -EncryptedMasterSecret $encrypted -Identifier 1 -IterationExponent 0 $decrypted | Should -Be $secret } It 'round-trips when Extendable is set' { $secret = Get-SecretSharingRandomByte -Length 16 $encrypted = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 999 -IterationExponent 0 -Extendable $decrypted = Unprotect-SecretSharingMasterSecret -EncryptedMasterSecret $encrypted -Identifier 999 -IterationExponent 0 -Extendable $decrypted | Should -Be $secret } It 'produces the same ciphertext for different identifiers when Extendable is set' { $secret = [byte[]](1..16) $a = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 1 -IterationExponent 0 -Extendable $b = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 2 -IterationExponent 0 -Extendable $a | Should -Be $b } It 'produces different ciphertext for different identifiers when not extendable' { $secret = [byte[]](1..16) $a = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 1 -IterationExponent 0 $b = Protect-SecretSharingMasterSecret -MasterSecret $secret -Identifier 2 -IterationExponent 0 ($a -join ',') | Should -Not -Be ($b -join ',') } It 'decrypting with the wrong passphrase silently returns the wrong secret rather than throwing' { $secret = Get-SecretSharingRandomByte -Length 16 $encrypted = Protect-SecretSharingMasterSecret -MasterSecret $secret -Passphrase ([System.Text.Encoding]::UTF8.GetBytes('right')) -Identifier 5 -IterationExponent 0 $decrypted = Unprotect-SecretSharingMasterSecret -EncryptedMasterSecret $encrypted -Passphrase ([System.Text.Encoding]::UTF8.GetBytes('wrong')) -Identifier 5 -IterationExponent 0 ($decrypted -join ',') | Should -Not -Be ($secret -join ',') } It 'throws when MasterSecret is shorter than the minimum length' { { Protect-SecretSharingMasterSecret -MasterSecret ([byte[]](1..8)) -Identifier 1 -IterationExponent 0 } | Should -Throw } It 'throws when MasterSecret has an odd length' { { Protect-SecretSharingMasterSecret -MasterSecret ([byte[]](1..17)) -Identifier 1 -IterationExponent 0 } | Should -Throw } } } } |