diff --git a/src/completers.ps1 b/src/completers.ps1 index b5926ee7..e38b730c 100644 --- a/src/completers.ps1 +++ b/src/completers.ps1 @@ -1,9 +1,9 @@ -Register-ArgumentCompleter -CommandName 'Get-Context', 'Set-Context', 'Remove-Context', 'Rename-Context' -ParameterName 'ID' -ScriptBlock { +Register-ArgumentCompleter -CommandName ($script:PSModuleInfo.FunctionsToExport) -ParameterName 'ID' -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - $script:Contexts.Values.ID | Where-Object { $_ -like "$wordToComplete*" } | - ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } + $contextInfos = Get-ContextInfo -ErrorAction SilentlyContinue -Verbose:$false -Debug:$false + $contextInfos | Where-Object { $_.ID -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.ID, $_.ID, 'ParameterValue', $_.ID) + } } diff --git a/src/functions/private/Import-Context.ps1 b/src/functions/private/Import-Context.ps1 deleted file mode 100644 index cbd8f3f3..00000000 --- a/src/functions/private/Import-Context.ps1 +++ /dev/null @@ -1,76 +0,0 @@ -#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.2' } - -filter Import-Context { - <# - .SYNOPSIS - Imports the context vault into memory. - - .DESCRIPTION - Imports all context files from the context vault directory into memory. - Each context is decrypted using the configured private key and stored - in the script-wide context collection for further use. - - .EXAMPLE - Import-Context - - Output: - ```powershell - VERBOSE: Importing contexts from vault: [C:\Vault] - VERBOSE: Found [3] contexts - VERBOSE: Importing context: [123456] - ``` - - Imports all contexts from the context vault into memory. - - .OUTPUTS - [pscustomobject]. - - .NOTES - Represents the imported context object containing ID, Path, and Context properties. - - .LINK - https://psmodule.io/Sodium/Functions/Import-Context/ - #> - [OutputType([object])] - [CmdletBinding()] - param() - - begin { - $stackPath = Get-PSCallStackPath - Write-Debug "[$stackPath] - Start" - if (-not $script:Config.Initialized) { - Set-ContextVault - } - } - - process { - try { - Write-Verbose "Importing contexts from vault: [$($script:Config.VaultPath)]" - $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse - Write-Verbose "Found [$($contextFiles.Count)] contexts" - $contextFiles | ForEach-Object { - $contextInfo = Get-Content -Path $_.FullName | ConvertFrom-Json - Write-Verbose "Importing context: [$($contextInfo.ID)]" - Write-Verbose ($contextInfo | Format-List | Out-String) - $params = @{ - SealedBox = $contextInfo.Context - PublicKey = $script:Config.PublicKey - PrivateKey = $script:Config.PrivateKey - } - $context = ConvertFrom-SodiumSealedBox @params - $script:Contexts[$contextInfo.ID] = [pscustomobject]@{ - ID = $contextInfo.ID - Path = $contextInfo.Path - Context = ConvertFrom-ContextJson -JsonString $context - } - } - } catch { - Write-Error $_ - throw 'Failed to get context' - } - } - - end { - Write-Debug "[$stackPath] - End" - } -} diff --git a/src/functions/private/Set-ContextVault.ps1 b/src/functions/private/Set-ContextVault.ps1 index c1fd6b5c..9e4e0f3a 100644 --- a/src/functions/private/Set-ContextVault.ps1 +++ b/src/functions/private/Set-ContextVault.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.2' } +#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' } function Set-ContextVault { <# @@ -7,7 +7,7 @@ function Set-ContextVault { .DESCRIPTION Sets the context vault. If the vault does not exist, it will be initialized. - Once the context vault is set, it will be imported into memory. + Once the context vault is set, the keys will be prepared for use. The vault consists of multiple security shards, including a machine-specific shard, a user-specific shard, and a seed shard stored within the vault directory. @@ -20,7 +20,7 @@ function Set-ContextVault { None. .NOTES - This function modifies the script-scoped configuration and imports the vault. + This function modifies the script-scoped configuration and prepares the vault for use. .LINK https://psmodule.io/Context/Functions/Set-ContextVault @@ -59,7 +59,7 @@ function Set-ContextVault { $machineShard = [System.Environment]::MachineName $userShard = [System.Environment]::UserName #$userInputShard = Read-Host -Prompt 'Enter a seed shard' # Eventually 4 shards. +1 for user input. - $seed = $machineShard + $userShard + $seedShard + $userInputShard + $seed = $machineShard + $userShard + $seedShard # + $userInputShard $keys = New-SodiumKeyPair -Seed $seed $script:Config.PrivateKey = $keys.PrivateKey $script:Config.PublicKey = $keys.PublicKey @@ -73,6 +73,5 @@ function Set-ContextVault { end { Write-Debug "[$stackPath] - End" - Import-Context } } diff --git a/src/functions/public/Get-Context.ps1 b/src/functions/public/Get-Context.ps1 index fdf6ecaf..f3537e58 100644 --- a/src/functions/public/Get-Context.ps1 +++ b/src/functions/public/Get-Context.ps1 @@ -1,10 +1,12 @@ -function Get-Context { +#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' } + +function Get-Context { <# .SYNOPSIS - Retrieves a context from the in-memory context vault. + Retrieves a context from the context vault. .DESCRIPTION - Retrieves a context from the loaded contexts stored in memory. + Retrieves a context by reading and decrypting context files directly from the vault directory. If no ID is specified, all available contexts will be returned. Wildcards are supported to match multiple contexts. @@ -42,7 +44,7 @@ LoginTime : 2/9/2025 10:45:11 AM ``` - Retrieves all contexts from the context vault (in memory). + Retrieves all contexts from the context vault (directly from disk). .EXAMPLE Get-Context -ID 'MySecret' @@ -68,7 +70,7 @@ YourData : {DataKey=DataValue} ``` - Retrieves all contexts that start with 'My' from the context vault (in memory). + Retrieves all contexts that start with 'My' from the context vault (directly from disk). .OUTPUTS [System.Object] @@ -106,7 +108,25 @@ Write-Verbose "Retrieving contexts - ID: [$($ID -join ', ')]" foreach ($item in $ID) { Write-Verbose "Retrieving contexts - ID: [$item]" - $script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExpandProperty Context + # Read context files directly from disk instead of using in-memory cache + $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse + foreach ($file in $contextFiles) { + try { + $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json + if ($contextInfo.ID -like $item) { + # Decrypt and return the context + $params = @{ + SealedBox = $contextInfo.Context + PublicKey = $script:Config.PublicKey + PrivateKey = $script:Config.PrivateKey + } + $contextObj = ConvertFrom-SodiumSealedBox @params + ConvertFrom-ContextJson -JsonString $contextObj + } + } catch { + Write-Warning "Failed to read or decrypt context file: $($file.FullName). Error: $_" + } + } } } diff --git a/src/functions/public/Get-ContextInfo.ps1 b/src/functions/public/Get-ContextInfo.ps1 index c89a0e0f..82eb8303 100644 --- a/src/functions/public/Get-ContextInfo.ps1 +++ b/src/functions/public/Get-ContextInfo.ps1 @@ -1,12 +1,13 @@ function Get-ContextInfo { <# .SYNOPSIS - Retrieves info about a context from the in-memory context vault. + Retrieves info about a context from the context vault. .DESCRIPTION - Retrieves info about context from the loaded contexts stored in memory. + Retrieves info about context files directly from the vault directory on disk. If no ID is specified, all available info on contexts will be returned. Wildcards are supported to match multiple contexts. + Only metadata (ID and Path) is returned without decrypting the context contents. .EXAMPLE Get-ContextInfo @@ -29,7 +30,7 @@ Path : ...\feacc853-5bea-48d1-b751-41ce9768d48e.json ``` - Retrieves all contexts from the context vault (in memory). + Retrieves all contexts from the context vault (directly from disk). .EXAMPLE Get-ContextInfo -ID 'MySecret' @@ -40,7 +41,7 @@ Path : ...\3e223259-f242-4e97-91c8-f0fd054cfea7.json ``` - Retrieves the context called 'MySecret' from the context vault (in memory). + Retrieves the context called 'MySecret' from the context vault (directly from disk). .EXAMPLE 'My*' | Get-ContextInfo @@ -57,7 +58,7 @@ Path : .../b7c01dbe-bccd-4c7e-b075-c5aac1c43b1a.json ``` - Retrieves all contexts that start with 'My' from the context vault (in memory). + Retrieves all contexts that start with 'My' from the context vault (directly from disk). .OUTPUTS [System.Object] @@ -94,7 +95,22 @@ process { Write-Verbose "Retrieving context info - ID: [$ID]" foreach ($item in $ID) { - $script:Contexts.Values | Where-Object { $_.ID -like $item } | Select-Object -ExcludeProperty Context + # Read context files directly from disk instead of using in-memory cache + $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse + foreach ($file in $contextFiles) { + try { + $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json + if ($contextInfo.ID -like $item) { + # Return only metadata (ID and Path), don't decrypt the Context property + [PSCustomObject]@{ + ID = $contextInfo.ID + Path = $contextInfo.Path + } + } + } catch { + Write-Warning "Failed to read context file: $($file.FullName). Error: $_" + } + } } } diff --git a/src/functions/public/Remove-Context.ps1 b/src/functions/public/Remove-Context.ps1 index f24ce43f..a8d94a57 100644 --- a/src/functions/public/Remove-Context.ps1 +++ b/src/functions/public/Remove-Context.ps1 @@ -98,18 +98,20 @@ process { foreach ($item in $ID) { Write-Verbose "Processing ID [$item]" - $script:Contexts.Keys | Where-Object { $_ -like $item } | ForEach-Object { - Write-Verbose "Removing context [$_]" - if ($PSCmdlet.ShouldProcess($_, 'Remove secret')) { - $script:Contexts[$_].Path | Remove-Item -Force - - Write-Verbose "Attempting to remove context: $_" - [PSCustomObject]$removedItem = $null - if ($script:Contexts.TryRemove($_, [ref]$removedItem)) { - Write-Verbose "Removed item: $removedItem" - } else { - Write-Verbose 'Key not found' + # Find contexts by scanning disk files instead of using in-memory cache + $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse + foreach ($file in $contextFiles) { + try { + $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json + if ($contextInfo.ID -like $item) { + Write-Verbose "Removing context [$($contextInfo.ID)]" + if ($PSCmdlet.ShouldProcess($contextInfo.ID, 'Remove secret')) { + $file.FullName | Remove-Item -Force + Write-Verbose "Removed context file: $($file.FullName)" + } } + } catch { + Write-Warning "Failed to read context file: $($file.FullName). Error: $_" } } } diff --git a/src/functions/public/Set-Context.ps1 b/src/functions/public/Set-Context.ps1 index 2a1e4f3d..143edf45 100644 --- a/src/functions/public/Set-Context.ps1 +++ b/src/functions/public/Set-Context.ps1 @@ -1,4 +1,4 @@ -#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.2' } +#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.2.0' } function Set-Context { <# @@ -7,8 +7,8 @@ function Set-Context { .DESCRIPTION If the context does not exist, it will be created. If it already exists, it will be updated. - The context is cached in memory for faster access. This function ensures that the context - is securely stored using encryption mechanisms. + The context is securely stored on disk using encryption mechanisms. + Each context operation reads the current state from disk to ensure consistency across processes. .EXAMPLE Set-Context -ID 'PSModule.GitHub' -Context @{ Name = 'MySecret' } @@ -84,16 +84,30 @@ function Set-Context { $ID = $Context.ID } if (-not $ID) { - throw "An ID is required, either as a parameter or as a property of the context object." + throw 'An ID is required, either as a parameter or as a property of the context object.' } - $existingContextInfo = $script:Contexts[$ID] - if (-not $existingContextInfo) { + $existingContextFile = $null + # Check if context already exists by scanning disk files + $contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse + foreach ($file in $contextFiles) { + try { + $contextInfo = Get-Content -Path $file.FullName | ConvertFrom-Json + if ($contextInfo.ID -eq $ID) { + $existingContextFile = $file + $Path = $contextInfo.Path + break + } + } catch { + Write-Warning "Failed to read context file: $($file.FullName). Error: $_" + } + } + + if (-not $existingContextFile) { Write-Verbose "Context [$ID] not found in vault" $Guid = [Guid]::NewGuid().ToString() $Path = Join-Path -Path $script:Config.VaultPath -ChildPath "$Guid.json" } else { Write-Verbose "Context [$ID] found in vault" - $Path = $existingContextInfo.Path } $contextJson = ConvertTo-ContextJson -Context $Context -ID $ID @@ -108,20 +122,6 @@ function Set-Context { if ($PSCmdlet.ShouldProcess($ID, 'Set context')) { Write-Verbose "Setting context [$ID] in vault" Set-Content -Path $Path -Value $param - $content = Get-Content -Path $Path - $contextInfoObj = $content | ConvertFrom-Json - $params = @{ - SealedBox = $contextInfoObj.Context - PublicKey = $script:Config.PublicKey - PrivateKey = $script:Config.PrivateKey - } - $contextObj = ConvertFrom-SodiumSealedBox @params - Write-Verbose ($contextObj | Format-List | Out-String) - $script:Contexts[$ID] = [PSCustomObject]@{ - ID = $ID - Path = $Path - Context = ConvertFrom-ContextJson -JsonString $contextObj - } } if ($PassThru) { diff --git a/src/variables/private/Config.ps1 b/src/variables/private/Config.ps1 index 0d65178f..9ab4afd4 100644 --- a/src/variables/private/Config.ps1 +++ b/src/variables/private/Config.ps1 @@ -1,7 +1,7 @@ $script:Config = [pscustomobject]@{ Initialized = $false # Has the vault been initialized? VaultPath = Join-Path -Path $HOME -ChildPath '.contextvault' # Vault directory path - SeedShardPath = 'vault.shard' # Seed shard path (relative to VaultPath) + SeedShardPath = 'vault.shard' # Seed shard path (relative to VaultPath) PrivateKey = $null # Private key (populated on init) PublicKey = $null # Public key (populated on init) } diff --git a/src/variables/private/Contexts.ps1 b/src/variables/private/Contexts.ps1 deleted file mode 100644 index a804bf0d..00000000 --- a/src/variables/private/Contexts.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -# Using a dictionary to get the benefit of reference type storage in PowerShell while multiple functions are running in parallel. -# Using concurrent dictionary to be able to safely access the dictionary from multiple threads /ForEach -Parallel. -$script:Contexts = [System.Collections.Concurrent.ConcurrentDictionary[string, [PSCustomObject]]]::new()