Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/functions/public/Get-Context.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.2' }
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.5' }

function Get-Context {
<#
Expand Down
2 changes: 1 addition & 1 deletion src/functions/public/Set-Context.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.2' }
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.5' }

function Set-Context {
<#
Expand Down
111 changes: 111 additions & 0 deletions tests/Context.Benchmark.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.5' }

<#
.SYNOPSIS
Performance benchmark for the Context module's core crypto operations.

.DESCRIPTION
Measures the time (in microseconds) per iteration for the three operations that every
Set-Context / Get-Context call executes: key-pair derivation, seal (encrypt), and
open (decrypt). Each scenario is repeated $Iterations times and the median is reported.

Run from the repo root after importing the module:

Import-Module ./src -Force
pwsh -NoProfile -File tests/Context.Benchmark.ps1

.OUTPUTS
PSCustomObject - one row per scenario with Scenario, Iterations, Median_µs, Min_µs, Max_µs.
#>
[CmdletBinding()]
param(
# Number of iterations per scenario.
[Parameter()]
[int] $Iterations = 1000,

# Vault name used during the benchmark (cleaned up automatically).
[Parameter()]
[string] $BenchmarkVault = 'Benchmark-Perf-Vault'
)

Set-StrictMode -Version Latest

function Measure-MedianMicroseconds {
param(
[scriptblock] $ScriptBlock,
[int] $Iterations
)

$samples = [System.Collections.Generic.List[double]]::new($Iterations)
for ($i = 0; $i -lt $Iterations; $i++) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
& $ScriptBlock | Out-Null
$sw.Stop()
$samples.Add($sw.Elapsed.TotalMilliseconds * 1000)
}
$sorted = $samples | Sort-Object
$mid = [int]($Iterations / 2)
return [pscustomobject]@{
Median_µs = [Math]::Round($sorted[$mid], 1)
Min_µs = [Math]::Round(($sorted | Select-Object -First 1), 1)
Max_µs = [Math]::Round(($sorted | Select-Object -Last 1), 1)
}
}

# Ensure bench vault exists and is clean
try {
Remove-ContextVault -Name $BenchmarkVault -Confirm:$false -ErrorAction SilentlyContinue
} catch {}
Set-ContextVault -Name $BenchmarkVault | Out-Null

$results = [System.Collections.Generic.List[pscustomobject]]::new()

Write-Host "Running Context benchmark ($Iterations iterations each)..." -ForegroundColor Cyan

# --- Key-pair derivation (New-SodiumKeyPair with seed) ---
$seed = 'BenchmarkSeedValue'
$kpStats = Measure-MedianMicroseconds -Iterations $Iterations -ScriptBlock {
New-SodiumKeyPair -Seed $seed
}
$results.Add([pscustomobject]@{
Scenario = 'New-SodiumKeyPair -Seed'
Iterations = $Iterations
Median_µs = $kpStats.Median_µs
Min_µs = $kpStats.Min_µs
Max_µs = $kpStats.Max_µs
})

# --- Seal (encrypt via Set-Context) ---
$sealStats = Measure-MedianMicroseconds -Iterations $Iterations -ScriptBlock {
Set-Context -ID 'bench-ctx' -Context @{ Value = 'benchmark' } -Vault $BenchmarkVault
}
$results.Add([pscustomobject]@{
Scenario = 'Set-Context (seal)'
Iterations = $Iterations
Median_µs = $sealStats.Median_µs
Min_µs = $sealStats.Min_µs
Max_µs = $sealStats.Max_µs
})

# --- Open (decrypt via Get-Context) ---
Set-Context -ID 'bench-ctx' -Context @{ Value = 'benchmark' } -Vault $BenchmarkVault
$openStats = Measure-MedianMicroseconds -Iterations $Iterations -ScriptBlock {
Get-Context -ID 'bench-ctx' -Vault $BenchmarkVault
}
$results.Add([pscustomobject]@{
Scenario = 'Get-Context (open/decrypt)'
Iterations = $Iterations
Median_µs = $openStats.Median_µs
Min_µs = $openStats.Min_µs
Max_µs = $openStats.Max_µs
})

# Cleanup
try {
Remove-ContextVault -Name $BenchmarkVault -Confirm:$false -ErrorAction SilentlyContinue
} catch {}

Write-Host ''
Write-Host '=== Context Benchmark Results ===' -ForegroundColor Green
$results | Format-Table -AutoSize
$results
229 changes: 229 additions & 0 deletions tests/Context.CrossVersionCompat.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<#
.SYNOPSIS
Cross-version compatibility test: Sodium 2.2.2 written data readable by Sodium 2.2.5.

.DESCRIPTION
Two-part test:

PART 1 — Crypto stability
Verifies that New-SodiumKeyPair -Seed produces identical keys in both Sodium versions,
and that a sealed box produced by 2.2.2 is decryptable by 2.2.5 (and vice versa).

PART 2 — Full stack (Context on-disk format)
Uses Context 8.1.3 (Sodium 2.2.2) to write a vault + contexts to disk, then uses
Sodium 2.2.5 directly to re-derive the same keys, read the raw JSON files, and
decrypt the stored sealed boxes — confirming the on-disk contract is preserved.

Each phase runs in a child pwsh process to ensure clean module state.
#>
[CmdletBinding()]
param(
[string] $VaultName = 'XVersionCompat-Test'
)

$ErrorActionPreference = 'Stop'
$tempDir = Join-Path $env:TEMP "sodium-compat-$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir | Out-Null

$cryptoResultFile = Join-Path $tempDir 'crypto.json'
$writeResultFile = Join-Path $tempDir 'write.json'
$readResultFile = Join-Path $tempDir 'read.json'

$pass = 0; $fail = 0

function Assert-Equal {
param($Name, $Expected, $Actual)
if ("$Actual" -eq "$Expected") {
Write-Host " [PASS] $Name" -ForegroundColor Green
$script:pass++
} else {
Write-Host " [FAIL] $Name" -ForegroundColor Red
Write-Host " expected : $Expected" -ForegroundColor Red
Write-Host " actual : $Actual" -ForegroundColor Red
$script:fail++
}
}
function Assert-Null {
param($Name, $Actual)
if ($null -eq $Actual -or "$Actual" -eq '') {
Write-Host " [PASS] $Name (null/empty as expected)" -ForegroundColor Green
$script:pass++
} else {
Write-Host " [FAIL] $Name — expected null/empty, got: [$Actual]" -ForegroundColor Red
$script:fail++
}
}

Write-Host ''
Write-Host '=== Cross-Version Sodium Compatibility Test ===' -ForegroundColor Cyan
Write-Host "Temp dir : $tempDir"
Write-Host ''

# ---------------------------------------------------------------------------
# PART 1 — CRYPTO STABILITY
# Confirms New-SodiumKeyPair -Seed is deterministic across versions,
# and that sealed boxes produced by 2.2.2 are openable by 2.2.5.
# ---------------------------------------------------------------------------
Write-Host '--- Part 1a: Seal message + derive keys with Sodium 2.2.2 ---' -ForegroundColor Yellow

$part1aScript = @"
`$ErrorActionPreference = 'Stop'
Import-Module Sodium -RequiredVersion 2.2.2 -Force
`$seed = 'CompatibilityTestSeedValue'
`$plaintext = 'Hello from Sodium 2.2.2'
`$kp = New-SodiumKeyPair -Seed `$seed
`$box22 = ConvertTo-SodiumSealedBox -Message `$plaintext -PublicKey `$kp.PublicKey
`$result = @{
PublicKey = `$kp.PublicKey
PrivateKey = `$kp.PrivateKey
SealedBox22 = `$box22
Plaintext = `$plaintext
}
`$result | ConvertTo-Json | Set-Content '$cryptoResultFile'
Write-Host "Keys derived and message sealed with Sodium 2.2.2"
"@

pwsh -NoProfile -Command $part1aScript
if (-not (Test-Path $cryptoResultFile)) { throw 'Part 1a failed: no result file' }
$c22 = Get-Content $cryptoResultFile -Raw | ConvertFrom-Json
Write-Host " PublicKey (2.2.2): $($c22.PublicKey)"
Write-Host " PrivateKey (2.2.2): $($c22.PrivateKey)"

Write-Host ''
Write-Host '--- Part 1b: Verify keys + unseal 2.2.2 message with Sodium 2.2.5 ---' -ForegroundColor Yellow

$part1bScript = @"
`$ErrorActionPreference = 'Stop'
Import-Module Sodium -RequiredVersion 2.2.5 -Force
`$seed = 'CompatibilityTestSeedValue'
`$kp25 = New-SodiumKeyPair -Seed `$seed
`$box22 = '$($c22.SealedBox22)'
`$pubKey = '$($c22.PublicKey)'
`$privKey = '$($c22.PrivateKey)'

# Decrypt the 2.2.2 ciphertext with 2.2.5
`$decrypted = ConvertFrom-SodiumSealedBox -SealedBox `$box22 -PublicKey `$pubKey -PrivateKey `$privKey

# Also seal a new message with 2.2.5
`$box25 = ConvertTo-SodiumSealedBox -Message 'Hello from Sodium 2.2.5' -PublicKey `$kp25.PublicKey
# Decrypt the 2.2.5 message immediately (roundtrip check)
`$rt25 = ConvertFrom-SodiumSealedBox -SealedBox `$box25 -PublicKey `$kp25.PublicKey -PrivateKey `$kp25.PrivateKey

`$result = @{
PublicKey25 = `$kp25.PublicKey
PrivateKey25 = `$kp25.PrivateKey
Decrypted22msg = `$decrypted
Roundtrip25msg = `$rt25
}
`$result | ConvertTo-Json | Set-Content ('$cryptoResultFile' -replace '\.json','-25.json')
Write-Host "Keys derived and 2.2.2 box decrypted with Sodium 2.2.5"
"@

$cryptoResult25File = $cryptoResultFile -replace '\.json', '-25.json'
pwsh -NoProfile -Command $part1bScript
if (-not (Test-Path $cryptoResult25File)) { throw 'Part 1b failed: no result file' }
$c25 = Get-Content $cryptoResult25File -Raw | ConvertFrom-Json

Write-Host " PublicKey (2.2.5): $($c25.PublicKey25)"
Write-Host " PrivateKey (2.2.5): $($c25.PrivateKey25)"
Write-Host " Decrypted 2.2.2 msg: $($c25.Decrypted22msg)"
Write-Host " 2.2.5 roundtrip msg: $($c25.Roundtrip25msg)"
Write-Host ''

Write-Host '--- Part 1 Assertions ---' -ForegroundColor Yellow
Assert-Equal 'Key derivation: PublicKey identical' $c22.PublicKey $c25.PublicKey25
Assert-Equal 'Key derivation: PrivateKey identical' $c22.PrivateKey $c25.PrivateKey25
Assert-Equal '2.2.2 sealed box decryptable by 2.2.5' $c22.Plaintext $c25.Decrypted22msg
Assert-Equal '2.2.5 roundtrip works' 'Hello from Sodium 2.2.5' $c25.Roundtrip25msg

# ---------------------------------------------------------------------------
# PART 2 — FULL STACK: Context 8.1.3 writes, Sodium 2.2.5 reads raw files
# ---------------------------------------------------------------------------
Write-Host ''
Write-Host '--- Part 2a: Write vault + contexts with Context 8.1.3 (Sodium 2.2.2) ---' -ForegroundColor Yellow

$part2aScript = @"
`$ErrorActionPreference = 'Stop'
Import-Module Sodium -RequiredVersion 2.2.2 -Force
Import-Module Context -Force

Get-ContextVault -Name '$VaultName' -ErrorAction SilentlyContinue | Remove-ContextVault -Confirm:`$false -ErrorAction SilentlyContinue
Set-ContextVault -Name '$VaultName' | Out-Null

Set-Context -ID 'compat-simple' -Context @{ Greeting = 'Hello'; Number = 42 } -Vault '$VaultName'
Set-Context -ID 'compat-secure' -Context @{ Token = ('secret123' | ConvertTo-SecureString -AsPlainText -Force) } -Vault '$VaultName'
Set-Context -ID 'compat-nulls' -Context @{ Present = 'yes'; Absent = `$null } -Vault '$VaultName'

`$vault = Get-ContextVault -Name '$VaultName'
`$shardPath = Join-Path `$vault.Path 'shard'
`$result = @{
VaultPath = `$vault.Path
ShardPath = `$shardPath
ShardContent = (Get-Content `$shardPath -Raw).Trim()
MachineName = [System.Environment]::MachineName
UserName = [System.Environment]::UserName
ContextFiles = (Get-ChildItem `$vault.Path -Filter '*.json' | Select-Object -ExpandProperty FullName)
}
`$result | ConvertTo-Json -Depth 5 | Set-Content '$writeResultFile'
Write-Host "Vault and contexts written with Context 8.1.3 / Sodium 2.2.2"
"@

pwsh -NoProfile -Command $part2aScript
if (-not (Test-Path $writeResultFile)) { throw 'Part 2a failed: no result file' }
$wr = Get-Content $writeResultFile -Raw | ConvertFrom-Json

Write-Host " VaultPath : $($wr.VaultPath)"
Write-Host " ContextFiles : $($wr.ContextFiles.Count) json files"
Write-Host ''
Write-Host '--- Part 2b: Decrypt raw vault files using Sodium 2.2.5 directly ---' -ForegroundColor Yellow

$contextFilesJson = ($wr.ContextFiles | ConvertTo-Json -Compress)

$part2bScript = @"
`$ErrorActionPreference = 'Stop'
Import-Module Sodium -RequiredVersion 2.2.5 -Force

# Re-derive the same keys as Context would
`$seed = '$($wr.MachineName)' + '$($wr.UserName)' + '$($wr.ShardContent)'
`$kp = New-SodiumKeyPair -Seed `$seed

`$contextFiles = '$($wr.ContextFiles -join "','")' -split "','"

`$results = @{}
foreach (`$file in `$contextFiles) {
`$json = Get-Content `$file -Raw | ConvertFrom-Json
`$plain = ConvertFrom-SodiumSealedBox -SealedBox `$json.Context -PublicKey `$kp.PublicKey -PrivateKey `$kp.PrivateKey
`$results[`$json.ID] = `$plain | ConvertFrom-Json -AsHashtable
}

`$results | ConvertTo-Json -Depth 10 | Set-Content '$readResultFile'
Write-Host "Raw vault files decrypted with Sodium 2.2.5"
"@

pwsh -NoProfile -Command $part2bScript
if (-not (Test-Path $readResultFile)) { throw 'Part 2b failed: no result file' }
$rd = Get-Content $readResultFile -Raw | ConvertFrom-Json

Write-Host '--- Part 2 Assertions ---' -ForegroundColor Yellow
Assert-Equal 'compat-simple: Greeting' 'Hello' $rd.'compat-simple'.Greeting
Assert-Equal 'compat-simple: Number' '42' $rd.'compat-simple'.Number
Assert-Equal 'compat-secure: Token (SecureString prefix)' '[SECURESTRING]secret123' $rd.'compat-secure'.Token
Assert-Equal 'compat-nulls: Present' 'yes' $rd.'compat-nulls'.Present
Assert-Null 'compat-nulls: Absent' $rd.'compat-nulls'.Absent

# Cleanup
Write-Host ''
try {
pwsh -NoProfile -Command "Import-Module Sodium -RequiredVersion 2.2.2 -Force; Import-Module Context -Force; Get-ContextVault -Name '$VaultName' -ErrorAction SilentlyContinue | Remove-ContextVault -Confirm:`$false -ErrorAction SilentlyContinue" 2>&1 | Out-Null
} catch {}
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue

$total = $pass + $fail
Write-Host ''
if ($fail -eq 0) {
Write-Host "=== RESULT: ALL $total ASSERTIONS PASSED ===" -ForegroundColor Green
Write-Host " Sodium 2.2.2 → 2.2.5: key derivation is identical, on-disk contract is preserved." -ForegroundColor Green
} else {
Write-Host "=== RESULT: $fail/$total ASSERTIONS FAILED — BREAKING CHANGE DETECTED ===" -ForegroundColor Red
}

Loading