New-SecureStoreSecret.ps1

<#
.SYNOPSIS
Creates or updates an encrypted secret in the SecureStore repository.

.DESCRIPTION
New-SecureStoreSecret derives or reuses an encryption key, protects the supplied secret
with authenticated AES encryption, and writes the payload using atomic file operations.
It honours ShouldProcess so you can preview writes with -WhatIf or require confirmation.

.PARAMETER KeyName
Logical name used to store the master encryption key (.bin file).

.PARAMETER SecretFileName
Name of the encrypted payload file stored beneath the secrets directory.

.PARAMETER Password
Secret value to protect. Accepts plain text or SecureString and is converted securely.

.PARAMETER FolderPath
Optional custom SecureStore base path. Defaults to the platform-specific root.

.INPUTS
System.String, System.Security.SecureString. Accepts pipeline input for -Password via property name.

.OUTPUTS
None. Writes files and emits verbose information.

.EXAMPLE
New-SecureStoreSecret -KeyName 'Database' -SecretFileName 'prod.secret' -Password 'P@ssw0rd!'

Creates or updates the secret named prod.secret using the Database master key.

.EXAMPLE
$secure = Read-Host 'Enter API token' -AsSecureString
New-SecureStoreSecret -KeyName 'Api' -SecretFileName 'token.secret' -Password $secure -Confirm:$false

Stores a SecureString value without prompting for confirmation.

.NOTES
Secrets are never written in plain text; in-memory buffers are cleared after use.

.LINK
Get-SecureStoreSecret
#>

function New-SecureStoreSecret {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'The parameter accepts SecureString and string for backwards compatibility; values are immediately converted to SecureString and zeroized after use.')]
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$KeyName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$SecretFileName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object]$Password,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$FolderPath = $script:DefaultSecureStorePath
    )

    begin {
        if (-not (Get-Command -Name 'Sync-SecureStoreWorkingDirectory' -ErrorAction SilentlyContinue)) {
            . "$PSScriptRoot/Sync-SecureStoreWorkingDirectory.ps1"
        }
    }

    process {
        $securePassword = $null
        $plaintextBytes = $null
        try {
            # Normalise the password input and resolve the SecureStore folder layout up front.
            $securePassword = ConvertTo-SecureStoreSecureString -InputObject $Password
            $paths = Sync-SecureStoreWorkingDirectory -BasePath $FolderPath

            if (Test-SecureStorePathLike -Value $KeyName) {
                $keyFilePath = Resolve-SecureStorePath -Path $KeyName -BasePath $paths.BasePath
            }
            else {
                $keyChild = if ($KeyName.EndsWith('.bin', [System.StringComparison]::OrdinalIgnoreCase)) { $KeyName } else { "$KeyName.bin" }
                $keyFilePath = Join-Path -Path $paths.BinPath -ChildPath $keyChild
            }

            $secretInputPath = if (Test-SecureStorePathLike -Value $SecretFileName) {
                Resolve-SecureStorePath -Path $SecretFileName -BasePath $paths.BasePath
            }
            else {
                Join-Path -Path $paths.SecretPath -ChildPath $SecretFileName
            }

            $secretFilePath = ConvertTo-SecureStorePreferredSecretPath -Path $secretInputPath -PreferredSecretDir $paths.SecretPath -LegacySecretDir $paths.LegacySecretPath

            if (-not $PSCmdlet.ShouldProcess($secretFilePath, "Create or update secure secret")) {
                return
            }

            $encryptionKey = $null
            $keyCreated = $false
            if (-not (Test-Path -LiteralPath $keyFilePath)) {
                # Generate a random 256-bit key when none exists so the secret is uniquely protected.
                $encryptionKey = New-Object byte[] 32
                $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
                try {
                    $rng.GetBytes($encryptionKey)
                }
                finally {
                    $rng.Dispose()
                }
                Write-Verbose "Generated new encryption key for '$KeyName'."
                Write-SecureStoreFile -Path $keyFilePath -Bytes $encryptionKey
                $keyCreated = $true
            }

            if (-not $encryptionKey) {
                # Reuse the existing key; this keeps reads and writes consistent for the same logical secret.
                $encryptionKey = Read-SecureStoreByteArray -Path $keyFilePath
            }

            try {
                # Convert the SecureString into bytes only for the duration of the encryption operation.
                $plaintextBytes = Get-SecureStorePlaintextData -SecureString $securePassword
                $payloadJson = Protect-SecureStoreSecret -Plaintext $plaintextBytes -MasterKey $encryptionKey
                $payloadBytes = [System.Text.Encoding]::UTF8.GetBytes($payloadJson)
                try {
                    # Atomic write prevents partially written secrets when the process stops unexpectedly.
                    Write-SecureStoreFile -Path $secretFilePath -Bytes $payloadBytes
                }
                finally {
                    [Array]::Clear($payloadBytes, 0, $payloadBytes.Length)
                }
            }
            finally {
                if ($null -ne $plaintextBytes) {
                    # Ensure plaintext remnants do not survive in memory after the write.
                    [Array]::Clear($plaintextBytes, 0, $plaintextBytes.Length)
                }
                if ($null -ne $encryptionKey) {
                    [Array]::Clear($encryptionKey, 0, $encryptionKey.Length)
                }
            }

            if ($keyCreated) {
                Write-Verbose "Encryption key '$KeyName' stored at '$keyFilePath'."
            }
        }
        catch {
            throw [System.InvalidOperationException]::new("Failed to create or update secret '$SecretFileName'.", $_.Exception)
        }
        finally {
            if ($securePassword) {
                $securePassword.Dispose()
            }
        }
    }
}