Secrecy.psm1


function Add-DynamicParam
{
<#
.SYNOPSIS
Adds a dynamic parameter to a script, within a DynamicParam block.
 
.DESCRIPTION
Adding dynamic parameters is a complex process, this attempts to simplify that.
 
.INPUTS
System.Object[] a list of possible values for this parameter to validate against.
 
.FUNCTIONALITY
PowerShell
 
.EXAMPLE
DynamicParam { Add-DynamicParam Path string -Mandatory; $DynamicParams } Process { Import-Variables $PSBoundParameters; ... }
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand','',
Justification='This script uses $input within an End block.')]
[CmdletBinding()][OutputType([void])] Param(
# The name of the parameter.
[Parameter(Position=0,Mandatory=$true)][string] $Name,
# The data type of the parameter.
[Parameter(Position=1)][type] $Type,
# The position of the parameter when not specifying the parameter names.
[int] $Position = -2147483648,
# The name of the set of parameters this parameter belongs to.
[string[]] $ParameterSetName = '__AllParameterSets',
# Alternate names for the parameter.
[string[]] $Alias,
<#
The valid number of values for a parameter that accepts a collection.
A range can be specified with a list of two integers.
#>

[Alias('Count')][ValidateCount(1,2)][int[]] $ValidateCount,
# Valid root drive(s) for parameters that accept paths.
[string[]] $ValidateDrive,
<#
The valid length for a string parameter.
A range can be specified with a list of two integers.
#>

[Alias('Length')][ValidateCount(1,2)][int[]] $ValidateLength,
# The valid regular expression pattern to match for a string parameter.
[Alias('Match','Pattern')][string] $ValidatePattern,
# The valid range of values for a numeric parameter.
[Alias('Range')][ValidateCount(2,2)][int[]] $ValidateRange,
<#
A script block to validate a parameter's value.
Any true result will validate the value, any false result will reject it.
#>

[ScriptBlock] $ValidateScript,
<#
A set of valid values for the parameter.
This will enable tab-completion.
#>

[Parameter(ValueFromPipeline=$true)][Alias('Values')][object[]] $ValidateSet,
# Requires parameter to be non-null.
[switch] $NotNull,
# Requires parameter to be non-null and non-empty.
[switch] $NotNullOrEmpty,
# Requires the parameter value to be Trusted data.
[switch] $TrustedData,
# Requires a path parameter to be on a User drive.
[switch] $UserDrive,
# Indicates a required parameter.
[Alias('Required')][switch] $Mandatory,
# Indicates a parameter that can accept values from the pipeline.
[Alias('Pipeline')][switch] $ValueFromPipeline,
<#
Indicates a parameter that can accept values from the pipeline by matching the property name of pipeline objects to the
parameter name or alias.
#>

[Alias('PipelineProperties','PipeName')][switch] $ValueFromPipelineByPropertyName,
# Indicates that the parameter will include any following positional parameters.
[Alias('RemainingArgs')][switch] $ValueFromRemainingArguments
)
End
{
    $DynamicParams = Get-Variable DynamicParams -Scope 1 -ErrorAction Ignore
    if($null -eq $DynamicParams)
    {
        $DynamicParams = New-Object Management.Automation.RuntimeDefinedParameterDictionary
        $DynamicParams = New-Variable DynamicParams $DynamicParams -Scope 1 -PassThru
    }
    $atts = New-Object Collections.ObjectModel.Collection[System.Attribute]
    foreach($set in $ParameterSetName)
    {
        $att = New-Object Management.Automation.ParameterAttribute -Property @{
            Position                        = $Position
            ParameterSetName                = $ParameterSetName
            Mandatory                       = $Mandatory
            ValueFromPipeline               = $ValueFromPipeline
            ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName
            ValueFromRemainingArguments     = $ValueFromRemainingArguments
        }
        $atts.Add($att)
    }
    if($Alias) {$atts.Add((New-Object Management.Automation.AliasAttribute $Alias))}
    if($NotNull) {$atts.Add((New-Object Management.Automation.ValidateNotNullAttribute))}
    if($NotNullOrEmpty) {$atts.Add((New-Object Management.Automation.ValidateNotNullOrEmptyAttribute))}
    if($ValidateCount)
    {
        if($ValidateCount.Length -eq 1) {$ValidateCount += $ValidateCount[0]}
        $atts.Add((New-Object Management.Automation.ValidateCountAttribute $ValidateCount))
    }
    if($ValidateDrive) {$atts.Add((New-Object Management.Automation.ValidateDriveAttribute $ValidateDrive))}
    if($ValidateLength)
    {
        if($ValidateLength.Length -eq 1) {$ValidateLength += $ValidateLength[0]}
        $atts.Add((New-Object Management.Automation.ValidateLengthAttribute $ValidateLength))
    }
    if($ValidatePattern) {$atts.Add((New-Object Management.Automation.ValidatePatternAttribute $ValidatePattern))}
    if($ValidateRange) {$atts.Add((New-Object Management.Automation.ValidateRangeAttribute $ValidateRange))}
    if($ValidateScript) {$atts.Add((New-Object Management.Automation.ValidateScriptAttribute $ValidateScript))}
    [psobject[]] $ValidateSet = $input
    if($ValidateSet) {$atts.Add((New-Object Management.Automation.ValidateSetAttribute $ValidateSet))}
    if($TrustedData) {$atts.Add((New-Object Management.Automation.ValidateTrustedDataAttribute))}
    if($UserDrive) {$atts.Add((New-Object Management.Automation.ValidateUserDriveAttribute))}
    $param = New-Object Management.Automation.RuntimeDefinedParameter ($Name,$Type,$atts)
    $DynamicParams.Value.Add($Name,$param)
}

}

function Export-SecretVault
{
<#
.SYNOPSIS
Exports secret vault content.
 
.OUTPUTS
System.Management.Automation.PSObject with these fields:
* Name: The secret name, used to identify the secret.
* Type: The data type of the secret.
* VaultName: Which vault the secret is stored in.
* Metadata: A simple hash (string to string/int/datetime) of extra secret context details.
 
.FUNCTIONALITY
Credential
 
.LINK
https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/
 
.EXAMPLE
Export-SecretVault |ConvertTo-Json |Out-File ~/secrets.json utf8
 
Backs up all secrets to a JSON file.
#>


[CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param()

filter Export-Credential
{
    [CmdletBinding()][OutputType([pscustomobject])] Param(
    [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserName,
    [Parameter(ValueFromPipelineByPropertyName=$true)][securestring] $Password
    )
    return [pscustomobject]@{
        UserName = $UserName
        Password = $Password |ConvertFrom-SecureString -AsPlainText
    }
}

filter Export-Secret
{
    [CmdletBinding()][OutputType([pscustomobject])] Param(
    [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Name,
    [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Type,
    [Parameter(ValueFromPipelineByPropertyName=$true)][string] $VaultName,
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Collections.ObjectModel.ReadOnlyDictionary[string,object]] $Metadata
    )
    return [pscustomobject]@{
        Name = $Name
        Type = $Type
        Value = switch($Type)
        {
            ByteArray {[Convert]::ToHexString((Get-Secret $Name -Vault $VaultName))}
            String {Get-Secret $Name -Vault $VaultName -AsPlainText}
            SecureString {Get-Secret $Name -Vault $VaultName -AsPlainText}
            PSCredential {Get-Secret $Name -Vault $VaultName |Export-Credential}
            Hashtable {Get-Secret $Name -Vault $VaultName -AsPlainText} # not yet supported
            default {Get-Secret $Name -Vault $VaultName -AsPlainText}
        }
        Vault = $VaultName
        Metadata = $Metadata
    }
}

if(!$PSCmdlet.ShouldProcess('secret vaults','export')) {return}
return @(Get-SecretInfo |Export-Secret)

}

function Get-SecretDetails
{
<#
.SYNOPSIS
Returns secret info from the secret vaults, including metadata as properties.
 
.PARAMETER Name
This parameter takes a String argument, including wildcard characters.
It is used to filter the search results that match on secret names the provided name pattern.
If no Name parameter argument is provided, then all stored secret metadata is returned.
 
.PARAMETER Vault
Optional parameter which takes a String argument that specifies a single vault to search.
 
.FUNCTIONALITY
Credential
 
.EXAMPLE
Get-SecretDetails
 
Name : test-creds
Type : PSCredential
VaultName : SecretStore
Title : Test
Description : Example credentials.
Note : Just for testing.
Uri : https://example.org/
Created : 2024-12-31 00:00:00
Expires : 2036-01-01 00:00:00
#>


[CmdletBinding()] Param()
DynamicParam
{
    Get-SecretInfo |Select-Object -ExpandProperty Name |Add-DynamicParam Name string -Position 0
    Get-SecretVault |Select-Object -ExpandProperty Name |Add-DynamicParam Vault string -Position 1
    $DynamicParams
}
Process
{
    Get-SecretInfo @PSBoundParameters |
        ForEach-Object {[pscustomobject][ordered]@{
            Name        = $_.Name
            Type        = $_.Type
            VaultName   = $_.VaultName
            Title       = $_.Metadata['Title']
            Description = $_.Metadata['Description']
            Note        = $_.Metadata['Note']
            Uri         = $_.Metadata['Uri']
            Created     = $_.Metadata['Created']
            Expires     = $_.Metadata['Expires']
        }}
}

}

function Import-SecretVault
{
<#
.SYNOPSIS
Imports secrets into secret vaults.
 
.NOTES
This is likely the configuration you'll need to run this:
Set-SecretStoreConfiguration -Scope CurrentUser -Authentication None -Interaction None
 
.INPUTS
System.Management.Automation.PSObject with these fields:
* Name: The secret name, used to identify the secret.
* Type: The data type of the secret.
* VaultName: Which vault the secret is stored in.
* Metadata: A simple hash (string to string/int/datetime) of extra secret context details.
 
.FUNCTIONALITY
Credential
 
.LINK
https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/
 
.EXAMPLE
Get-Content ~/secrets.json |ConvertFrom-Json |Import-SecretVault
 
Restores secrets to vaults.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText','',
Justification='This script exports secrets.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword','',
Justification='This script exports secrets.')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams','',
Justification='This script exports secrets.')]
[CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param(
[Parameter(ValueFromPipelineByPropertyName=$true)][string] $Name,
[Parameter(ValueFromPipelineByPropertyName=$true)][string] $Type,
[Parameter(ValueFromPipelineByPropertyName=$true)][psobject] $Value,
[Parameter(ValueFromPipelineByPropertyName=$true)][string] $Vault,
[Parameter(ValueFromPipelineByPropertyName=$true)][psobject] $Metadata
)
Begin
{
    filter ConvertTo-Credential
    {
        [CmdletBinding()] Param(
        [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserName,
        [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Password
        )
        return New-Object PSCredential $UserName,(ConvertTo-SecureString $Password -AsPlainText -Force)
    }
}
Process
{
    if(!(Get-SecretVault $Vault -ErrorAction Ignore))
    {
        Register-SecretVault -Name $Vault -ModuleName Microsoft.PowerShell.SecretStore
    }
    $meta = @($Metadata.PSObject.Properties).Count ? @{Metadata=$Metadata |ConvertTo-Json |ConvertFrom-Json -AsHashtable} : @{}
    foreach($k in $meta.Keys) {if($meta[$k] -is [long]){$meta[$k] = [int]$meta[$k]}}
    switch($Type)
    {
        ByteArray {Set-Secret $Name ([Convert]::FromHexString($Value)) -Vault $Vault @meta}
        String {Set-Secret $Name $Value -Vault $Vault @meta}
        SecureString {Set-Secret $Name (ConvertTo-SecureString $Value -AsPlainText -Force) -Vault $Vault @meta}
        PSCredential {Set-Secret $Name ($Value |ConvertTo-Credential) -Vault $Vault @meta}
        #Hashtable {Set-Secret $Name ($Value |ConvertTo-Json |ConvertFrom-Json -AsHashtable) -Vault $Vault @meta} # not yet supported
        default {Set-Secret $Name $Value -Vault $Vault @meta}
    }
}

}

function Initialize-SecretVault
{
<#
.SYNOPSIS
Sets up secret storage for the first time.
 
.FUNCTIONALITY
Credential
 
.LINK
https://devblogs.microsoft.com/powershell/secretmanagement-and-secretstore-are-generally-available/
 
.EXAMPLE
Initialize-SecretVault
 
 
#>


#Requires -Version 7
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] Param(
# Disable password prompts
[Alias('Headless')][switch] $HandsFree
)
if(Test-SecretVault) {Write-Information 'Secret vault is already set up.'; return}
if(!$HandsFree) {Set-SecretStoreConfiguration -Default}
else
{
    if(!$PSCmdlet.ShouldProcess('secret vault','set up without a password')) {return}
    Set-SecretStoreConfiguration -Authentication None -Interaction None
}
Register-SecretVault -Name SecretVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
if(Test-SecretVault) {Write-Information 'Secret vault has been set up!'}

}

function Set-SecretDetails
{
<#
.SYNOPSIS
Sets a secret in a secret vault with metadata.
 
.FUNCTIONALITY
Credential
 
.EXAMPLE
Set-SecretDetails GitHubToken -Paste securestring -Title 'PowerShell token' -Description 'A GitHub classic token' -Url https://github.com/settings/tokens -Expires (Get-Date).AddDays(90)
 
Stores the token from the clipboard.
#>


[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText','',
Justification='The data source is plaintext. SecureString benefits may be in dispute: <https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md>')]
[CmdletBinding()] Param(
# Specifies the name of the secret to add metadata to. Wildcard characters (`*`) are not permitted.
[Parameter(Position=0,Mandatory=$true)][string] $Name,
# Specifies the value of the secret.
[Parameter(ParameterSetName='Secret',Position=1,Mandatory=$true)][securestring] $Secret,
# Specifies the value of the credential to store.
[Parameter(ParameterSetName='Credential',Position=1,Mandatory=$true)][pscredential] $Credential,
# Title metadata field.
[string] $Title,
# Description metadata field.
[string] $Description,
# Note metadata field.
[string] $Note,
# Uri metadata field.
[uri] $Uri,
# Created date/time metadata field.
[datetime] $Created = (Get-Date),
# Expiration date/time metadata field.
[datetime] $Expires,
# Specifies the type to interpret the text on the clipboard as for use as the secret value.
[Parameter(ParameterSetName='Paste',Mandatory=$true)]
[ValidateSet('string','securestring','bytes','hexbytes')][string] $Paste,
# Specifies the username to combine with the clipboard text as a password to store as a credential secret.
[Parameter(ParameterSetName='PasteForUser',Mandatory=$true)][string] $PasteForUser,
# Specifies the encoding to read the clipboard as, into a byte array secret.
[Parameter(ParameterSetName='PasteTextBytes',Mandatory=$true)][Text.Encoding] $PasteTextBytes
)
$clipboard = Get-Clipboard |Out-String
$value = switch($PSCmdlet.ParameterSetName)
{
    PasteForUser {New-Object pscredential $PasteForUser,($clipboard |ConvertTo-SecureString -AsPlainText -Force)}
    PasteTextBytes {$PasteTextBytes.GetBytes($clipboard)}
    Secret {$Secret}
    Credential {$Credential}
    Paste
    {
        switch($Paste)
        {
            string {$clipboard}
            securestring {$clipboard |ConvertTo-SecureString -AsPlainText -Force}
            bytes {[byte[]]($clipboard -split '\D+')}
            hexbytes {
                $clipboard = $clipboard -replace '[^A-Fa-f0-9]+'
                $bytes = [bigint]::Parse("00$clipboard",'HexNumber').ToByteArray()
                [array]::Reverse($bytes)
                $null,$bytes = $bytes
                $bytes
            }
        }
    }
}
$metadata = @{ Created = $Created }
if($Title) {$metadata['Title'] = $Title}
if($Description) {$metadata['Description'] = $Description}
if($Note) {$metadata['Note'] = $Note}
if($Uri) {$metadata['Uri'] = "$Uri"}
if($Expires) {$metadata['Expires'] = $Expires}
Set-Secret $Name $value -Metadata $metadata

}
Export-ModuleMember -Function Export-SecretVault,Get-SecretDetails,Import-SecretVault,Initialize-SecretVault,Set-SecretDetails