Skip to content

Commit c89b338

Browse files
🌟 [Major]: Replace SecretStore with custom Sodium based vault (#76)
## Description 🌟 [Major]: All current stored contexts will not be accessible from the Context module after updating. This pull request includes significant changes to the context management system, mainly focusing on transitioning from the `Microsoft.PowerShell.SecretManagement` module to a local implementation and using `Sodium` for encryption of secrets. The changes impact multiple functions, including context management and configuration. ### Transition to `Sodium` module: * [`src/functions/private/Import-Context.ps1`](diffhunk://#diff-fc68944c31a324729fbd1eff057b48bee52d49c57145f1fa3ac7eef0dc092b10L1-R1): Updated the module requirement to `Sodium` and modified the context import logic to use JSON files and Sodium encryption. [[1]](diffhunk://#diff-fc68944c31a324729fbd1eff057b48bee52d49c57145f1fa3ac7eef0dc092b10L1-R1) [[2]](diffhunk://#diff-fc68944c31a324729fbd1eff057b48bee52d49c57145f1fa3ac7eef0dc092b10L30-L38) * [`src/functions/private/Set-ContextVault.ps1`](diffhunk://#diff-406313c23db28018a899056d564dcfc471e582e673440c9659a2278316a03673L1-R18): Changed the module requirement to `Sodium` and updated the vault initialization logic to use Sodium key pairs instead of SecretStore. [[1]](diffhunk://#diff-406313c23db28018a899056d564dcfc471e582e673440c9659a2278316a03673L1-R18) [[2]](diffhunk://#diff-406313c23db28018a899056d564dcfc471e582e673440c9659a2278316a03673L56-L96) * [`src/functions/public/Set-Context.ps1`](diffhunk://#diff-d12895be2e58b33d275f1d10ec54bd8ee0b555bef81f53d6e39ca1722ea58f44L1-R1): Updated the module requirement to `Sodium` and modified the context setting logic to use Sodium encryption and JSON files. [[1]](diffhunk://#diff-d12895be2e58b33d275f1d10ec54bd8ee0b555bef81f53d6e39ca1722ea58f44L1-R1) [[2]](diffhunk://#diff-d12895be2e58b33d275f1d10ec54bd8ee0b555bef81f53d6e39ca1722ea58f44L51-R102) ### Configuration changes: * [`src/variables/private/Config.ps1`](diffhunk://#diff-24f682ffe9ba06c3a7c6620f5c550ddc9eac8bdfb1aefe07961c41cf1a5e9552L2-R6): Updated the configuration to include new properties for vault path, seed shard path, and keys. * [`src/variables/private/Contexts.ps1`](diffhunk://#diff-b43b49765b90168517446908d901969f395835561822625c504c8e5ac52e78f1L1-R3): Changed the context storage to use a concurrent dictionary for thread-safe operations. ### Function updates: * [`src/functions/public/Get-Context.ps1`](diffhunk://#diff-0d7db8bc789bd7b13874d319644d59a93cd442baa4da9f9a20854434d7a2b41aL32-R35): Simplified the parameter definition for the `Get-Context` function and updated the logic to retrieve contexts. [[1]](diffhunk://#diff-0d7db8bc789bd7b13874d319644d59a93cd442baa4da9f9a20854434d7a2b41aL32-R35) [[2]](diffhunk://#diff-0d7db8bc789bd7b13874d319644d59a93cd442baa4da9f9a20854434d7a2b41aL52-R53) * [`src/functions/public/Remove-Context.ps1`](diffhunk://#diff-bb64b18ab5c85e130b2713ff666adbabfafe59137c69eb19b1375d410872fbabL1-R1): Removed the module requirement for SecretManagement and updated the context removal logic to handle file-based contexts. [[1]](diffhunk://#diff-bb64b18ab5c85e130b2713ff666adbabfafe59137c69eb19b1375d410872fbabL1-R1) [[2]](diffhunk://#diff-bb64b18ab5c85e130b2713ff666adbabfafe59137c69eb19b1375d410872fbabL65-R66) ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [x] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas
1 parent 9784f75 commit c89b338

18 files changed

+641
-244
lines changed

.github/workflows/Process-PSModule.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ jobs:
2828
Process-PSModule:
2929
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v3
3030
secrets: inherit
31+
with:
32+
Verbose: true
33+
Debug: true

README.md

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,27 @@ we aim to store this data using a the concept of `Contexts` that are stored loca
55
developers separate user and module data from the module code, so that modules can be created in a way where users can resume from where they left off
66
without having to reconfigure the module or log in to services that support refreshing sessions with data you can store, i.e., refresh tokens.
77

8+
The module uses NaCl based encryption delivered by the `libsodium` library to encrypt and decrypt the data stored in a `Context`. The module that
9+
serves this functionality is called [`Sodium`](https://github.com/someuser/Sodium) and is a dependency of this module. The
10+
[`Sodium`](https://github.com/someuser/Sodium) module is automatically installed when you install this module.
11+
812
## What is a `Context`?
913

10-
The consept of `Contexts` is built on top of the functionality provided by the `Microsoft.PowerShell.SecretManagement` and
11-
`Microsoft.PowerShell.SecretStore` modules. The `Context` module manages a set of `secrets` that is stored in a `SecretVault` instance. A context in
12-
this case is a data structure that supports secrets and regular datatypes converted to a modified JSON structure and stored as a string based secret
13-
in the `SecretStore`. The `Context` is stored in the `SecretVault` as a secret with the name `Context:<ContextId>`.
14+
A `Context` is a way to persist user and module state securely on disk while ensuring data remains encrypted at rest. It stores structured data that
15+
can be represented in JSON format, including regular values and secure secrets. It can hold multiple secrets (such as passwords or API tokens)
16+
alongside general data like configuration settings, session metadata, or user preferences. Secure secrets are specially handled to maintain their
17+
security when stored and retrieved.
1418

15-
The context is stored as compressed JSON and could look something like the examples below. These are the same data but one shows the JSON structure
16-
that is stored in the `SecretStore` and the other shows the same data as a `PSCustomObject` that could be used in a PowerShell script.
19+
When saving a `Context`, the data is converted to a plain-text JSON structure, then encrypted and written to disk. Secure strings are marked with a
20+
special prefix before encryption, ensuring they can be safely restored as secure strings when the `Context` is loaded back into memory.
1721

18-
The ID of the context is the name of the secret that it is stored as in the `SecretStore`. The ID is also stored with the context as a property in the
19-
context data structure. This is to make it easier to find the context when you only have the context object.
22+
When imported, the encrypted data is decrypted, converted back into its original structured format, and held in memory, ensuring both usability and
23+
security.
2024

2125
<details>
22-
<summary>Input to Set-Context - As a PSCustomObject</summary>
26+
<summary>1. Storing data (object or dictionary) in persistent storage using Set-Context</summary>
2327

2428
Typical the first input to a context (altho it can also be a hashtable or any other object type that converts with JSON)
25-
This object should NOT have a property on root of the object called `ID` as this is added by the module.
2629

2730
```pwsh
2831
Set-Context -ID 'john_doe' -Context ([PSCustomObject]@{
@@ -92,14 +95,14 @@ Set-Context -ID 'john_doe' -Context ([PSCustomObject]@{
9295
</details>
9396

9497
<details>
95-
<summary>The stored data in a secret - As a processed JSON</summary>
98+
<summary>2. How the data utimatly gets stored - As a processed JSON</summary>
9699

97100
This is how the objecet above is stored, except that this is an uncomressed version for readability.
98-
Here you see that the `ID` property is added.
101+
Here you see that the `ID` property gets added.
99102

100103
```json
101104
{
102-
"ID": "Context:john_doe",
105+
"ID": "john_doe",
103106
"Username": "john_doe",
104107
"AuthToken": "[SECURESTRING]ghp_12345ABCDE67890FGHIJ",
105108
"LoginTime": "2024-11-21T21:16:56.2518249+01:00",
@@ -184,15 +187,15 @@ Here you see that the `ID` property is added.
184187
</details>
185188

186189
<details>
187-
<summary>Output from Get-Context - As a PSCustomObject</summary>
190+
<summary>3. Imported data (as a PSCustomObject) and shown with Get-Context</summary>
188191

189192
This is how the object is returned from the `Get-Context` function.
190193
Notice that the `ID` property has been added to the object.
191194

192195
```pwsh
193196
Get-Context -ID 'john_doe'
194197
195-
ID : Context:john_doe
198+
ID : john_doe
196199
UserPreferences : @{DefaultBranch=main; Notifications=; Theme=dark; CodeReview=System.Object[]}
197200
LastLoginAttempts : {@{Success=True; IP=System.Security.SecureString; Timestamp=11/24/2024 2:09:12 PM}, @{Success=False; IP=System.Security.SecureString; Timestamp=11/23/2024 3:09:12 PM}}
198201
IsTwoFactorAuth : True
@@ -208,12 +211,6 @@ AccessScopes : {repo, user, gist, admin:org}
208211
```
209212
</details>
210213

211-
## Prerequisites
212-
213-
This module relies on [Microsoft.PowerShell.SecretManagement](https://github.com/powershell/SecretManagement) and
214-
[Microsoft.PowerShell.SecretStore](https://github.com/PowerShell/SecretStore). The module automatically installs these modules if they are not
215-
already installed.
216-
217214
## Installation
218215

219216
Install the module from the PowerShell Gallery by running the following command:
@@ -230,33 +227,33 @@ Lets have a look at how to use the module to store these types of data in abit m
230227

231228
### Module settings
232229

233-
To store module data, the module developer can create a context that defines a "namespace" for the module. This context can store settings and secrets
234-
for the module. A module developer can also create additional contexts for additional settings that share the same lifecycle, like settings
230+
To store module data, the module developer can create a `Context` that defines a "namespace" for the module. This `Context` can store settings
231+
for the module. A module developer can also create additional `Contexts` for additional settings that share the same lifecycle, like settings
235232
associated with a module extension.
236233

237-
Let's say we have a module called `GitHub` that needs to store some settings and secrets. The module developer could initialize a context called
238-
`GitHub` as part of the loading section in the module code. All module configuration could be stored in this context by using the functionality in
239-
this module. The context for the module is stored in the `SecretVault` as a secret with the name `Context:GitHub`.
234+
Let's say we have a module called `GitHub` that needs to store some settings. The module developer could initialize a `Context` called
235+
`GitHub` as part of the loading section in the module code. All module configuration could be stored in this `Context` by using the functionality in
236+
this module. The context for the module is stored in the `ContextVault` as a `Context` with the ID `GitHub`.
240237

241238
### User Configuration
242239

243-
To store user data, the module developer can create a new context that defines a "namespace" for the user configuration. So let's say a developer has
240+
To store user data, the module developer can create a new `Context` that defines a "namespace" for the user configuration. So let's say a developer has
244241
implemented this for the `GitHub` module, a user would log in using their details. The module would call upon `Context` functionality to create a new
245-
context under the `GitHub` namespace.
242+
`Context` under the `GitHub` namespace.
246243

247-
Imagine a user called `BobMarley` logs in to the `GitHub` module. The following would exist in the context:
244+
Imagine a user called `BobMarley` logs in to the `GitHub` module. The following would exist in the `ContextVault`:
248245

249-
- `Context:GitHub` containing module configuration, like default user, host, and client ID to use if not otherwise specified.
250-
- `Context:GitHub/BobMarley` containing user configuration, details about the user, secrets and default values for API calls etc.
246+
- `GitHub` containing module configuration, like default user, host, and client ID to use if not otherwise specified.
247+
- `GitHub/BobMarley` containing user configuration, details about the user, secrets and default values for API calls etc.
251248

252-
Let's say the person also has another account on `GitHub` called `RastaBlasta`. After logging on with the second account, the following context would
253-
also exist in the context:
249+
Let's say the person also has another account on `GitHub` called `LennyKravitz`. After logging on the second account, the following `Context` would
250+
also exist in the `ContextVault`:
254251

255-
- `Context:GitHub/RastaBlasta` containing user configuration, details about the user, secrets and default values for API calls etc.
252+
- `GitHub/LennyKravitz` containing user configuration, details about the user, secrets and default values for API calls etc.
256253

257-
With this the module developer could allow users to set default context, and store a key of the name of that context in the module context. This way
258-
the module could automatically log in the user to the correct account when the module is loaded. The user could also switch between accounts by
259-
changing the default context.
254+
With this the module developer could allow users to set default `Context`, and store a key of the name of that `Context` in the module `Context`. This
255+
way the module could automatically log in the user to the correct account when the module is loaded. The user could also switch between accounts by
256+
changing the default `Context`.
260257

261258
### Setup for a New Module
262259

@@ -291,21 +288,21 @@ try {
291288
2. Add some module configuration -> `$context = Get-Context -ID 'GitHub'` -> Change settings using the returned object and
292289
then `Set-Context -ID 'GitHub' -Context $context` to store the changes.
293290

294-
### Setup for a New Context
291+
### Setup for a New user context
295292

296293
To set up a new context for a user, the following steps should be taken:
297294

298-
1. Create a set of integration functions that you can expose to the user and that uses the `Context` module to store user data. Its highly recommended
299-
to do this so that you as a module developer can create the structure you want for the context, while also giving the user the expected function
295+
1. Create a set of public integration functions that uses the `Context` module to store user data. Its highly recommended
296+
to do this so that you as a module developer can create the structure you want for the `Context`, while also giving the user the expected function
300297
names to interact with the module.
301298
- `Set-<ModuleName>Context` that uses `Set-Context`.
302299
- `Get-<ModuleName>Context` that uses `Get-Context`.
303300
- `Remove-<ModuleName>Context` that uses `Remove-Context`
304301

305-
2. Create a new context for the user -> `Set-Context -ID 'GitHub.BobMarley'` -> Context `GitHub/BobMarley` is created.
302+
2. Create a new `Context` for the user `Connect-GitHub ...` -> `Set-Context -ID 'GitHub.BobMarley'` -> `Context` `GitHub/BobMarley` is created.
306303
3. Add some user configuration -> `$context = Get-Context -ID 'GitHub.BobMarley'` -> Change settings using the returned object and
307304
then `Set-Context -ID 'GitHub.BobMarley' -Context $context` to store the changes.
308-
4. Get the user configuration -> `Get-Context -Context 'GitHub/BobMarley'` -> The context object is returned, and you can access the data in it.
305+
4. Get the user configuration -> `Get-Context -Context 'GitHub/BobMarley'` -> The `Context` object is returned, and you can access the data in it.
309306

310307
## Contributing
311308

@@ -323,6 +320,4 @@ If you do code, we'd love to have your contributions. Please read the [Contribut
323320

324321
## Links
325322

326-
- SecretManagement | [GitHub](https://GitHub.com/powershell/SecretManagement) | [Docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement/?view=ps-modules)
327-
- SecretStore | [GitHub](https://GitHub.com/PowerShell/SecretStore) | [Docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretstore/?view=ps-modules)
328-
- [Overview of the SecretManagement and SecretStore modules | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/overview?view=ps-modules)
323+
- Sodium [GitHub](https://github.com/someuser/Sodium) | [PSGallery](https://www.powershellgallery.com/packages/Sodium)

src/functions/private/Import-Context.ps1

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
1-
#Requires -Modules @{ ModuleName = 'Microsoft.PowerShell.SecretManagement'; RequiredVersion = '1.1.2' }
1+
#Requires -Modules @{ ModuleName = 'Sodium'; RequiredVersion = '2.1.1' }
22

33
filter Import-Context {
44
<#
55
.SYNOPSIS
66
Imports the context vault into memory.
77
88
.DESCRIPTION
9-
Imports the context vault into memory.
9+
Imports all context files from the context vault directory into memory.
10+
Each context is decrypted using the configured private key and stored
11+
in the script-wide context collection for further use.
1012
1113
.EXAMPLE
1214
Import-Context
1315
16+
Output:
17+
```powershell
18+
VERBOSE: Importing contexts from vault: [C:\Vault]
19+
VERBOSE: Found [3] contexts
20+
VERBOSE: Importing context: [123456]
21+
```
22+
1423
Imports all contexts from the context vault into memory.
24+
25+
.OUTPUTS
26+
[pscustomobject].
27+
28+
.NOTES
29+
Represents the imported context object containing ID, Path, and Context properties.
30+
31+
.LINK
32+
https://psmodule.io/Sodium/Functions/Import-Context/
1533
#>
1634
[OutputType([object])]
1735
[CmdletBinding()]
@@ -27,15 +45,25 @@ filter Import-Context {
2745

2846
process {
2947
try {
30-
Write-Verbose "Importing contexts: [$($script:Config.SecretPrefix)*] from vault: $($script:Config.VaultName)"
31-
$secretInfos = Get-SecretInfo -Name "$($script:Config.SecretPrefix)*" -Vault $script:Config.VaultName -Verbose:$false
32-
Write-Verbose "Found [$($secretInfos.Count)] secrets"
33-
$secretInfos | ForEach-Object {
34-
Write-Verbose "- [$($_.Name)]"
35-
$secretJson = $_ | Get-Secret -AsPlainText -Verbose:$false
36-
$script:Contexts[$_.Name] = ConvertFrom-ContextJson -JsonString $secretJson
48+
Write-Verbose "Importing contexts from vault: [$($script:Config.VaultPath)]"
49+
$contextFiles = Get-ChildItem -Path $script:Config.VaultPath -Filter *.json -File -Recurse
50+
Write-Verbose "Found [$($contextFiles.Count)] contexts"
51+
$contextFiles | ForEach-Object {
52+
$contextInfo = Get-Content -Path $_.FullName | ConvertFrom-Json
53+
Write-Verbose "Importing context: [$($contextInfo.ID)]"
54+
Write-Verbose ($contextInfo | Format-List | Out-String)
55+
$params = @{
56+
SealedBox = $contextInfo.Context
57+
PublicKey = $script:Config.PublicKey
58+
PrivateKey = $script:Config.PrivateKey
59+
}
60+
$context = ConvertFrom-SodiumSealedBox @params
61+
$script:Contexts[$contextInfo.ID] = [pscustomobject]@{
62+
ID = $contextInfo.ID
63+
Path = $contextInfo.Path
64+
Context = ConvertFrom-ContextJson -JsonString $context
65+
}
3766
}
38-
3967
} catch {
4068
Write-Error $_
4169
throw 'Failed to get context'

src/functions/private/JsonToObject/Convert-ContextHashtableToObjectRecursive.ps1

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
function Convert-ContextHashtableToObjectRecursive {
22
<#
33
.SYNOPSIS
4-
Converts a hashtable to a context object.
4+
Converts a hashtable into a structured context object.
55
66
.DESCRIPTION
7-
This function is used to convert a hashtable to a context object.
8-
String values that are prefixed with '[SECURESTRING]', are converted back to SecureString objects.
9-
Other values are converted to their original types, like ints, booleans, string, arrays, and nested objects.
7+
This function recursively converts a hashtable into a structured PowerShell object.
8+
String values prefixed with '[SECURESTRING]' are converted back to SecureString objects.
9+
Other values retain their original data types, including integers, booleans, strings, arrays,
10+
and nested objects.
1011
1112
.EXAMPLE
1213
Convert-ContextHashtableToObjectRecursive -Hashtable @{
@@ -18,17 +19,38 @@
1819
}
1920
}
2021
21-
This example converts a hashtable to a context object, where the 'Token' and 'Nested.Token' values are SecureString objects.
22+
Output:
23+
```powershell
24+
Name : Test
25+
Token : System.Security.SecureString
26+
Nested : @{ Name = Nested; Token = System.Security.SecureString }
27+
```
28+
29+
This example converts a hashtable into a structured object, where 'Token' and 'Nested.Token'
30+
values are SecureString objects.
31+
32+
.OUTPUTS
33+
PSCustomObject.
34+
35+
.NOTES
36+
Returns an object where values are converted to their respective types,
37+
including SecureString for sensitive values, arrays for list structures, and nested objects
38+
for hashtables.
39+
40+
.LINK
41+
https://psmodule.io/Context/Functions/Convert-ContextHashtableToObjectRecursive
2242
#>
43+
2344
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
2445
'PSAvoidUsingConvertToSecureStringWithPlainText', '',
25-
Justification = 'The securestring is read from the object this function reads.'
46+
Justification = 'The SecureString is extracted from the object being processed by this function.'
2647
)]
2748
[OutputType([pscustomobject])]
2849
[CmdletBinding()]
2950
param (
30-
# Hashtable to convert to context object
31-
[object] $Hashtable
51+
# Hashtable to convert into a structured context object
52+
[Parameter(Mandatory)]
53+
[hashtable] $Hashtable
3254
)
3355

3456
begin {
@@ -53,7 +75,7 @@
5375
Write-Debug "Converting [$key] as [hashtable]"
5476
$result | Add-Member -NotePropertyName $key -NotePropertyValue (Convert-ContextHashtableToObjectRecursive $value)
5577
} elseif ($value -is [array]) {
56-
Write-Debug "Converting [$key] as [IEnumerable], including arrays and hashtables"
78+
Write-Debug "Converting [$key] as [array], processing elements individually"
5779
$result | Add-Member -NotePropertyName $key -NotePropertyValue @(
5880
$value | ForEach-Object {
5981
if ($_ -is [hashtable]) {
@@ -64,7 +86,7 @@
6486
}
6587
)
6688
} else {
67-
Write-Debug "Converting [$key] as regular value"
89+
Write-Debug "Adding [$key] as a standard value"
6890
$result | Add-Member -NotePropertyName $key -NotePropertyValue $value
6991
}
7092
}

0 commit comments

Comments
 (0)