Public/Secrets/Set-IDBridgeSecret.ps1

<#
.SYNOPSIS
Add or change a named secret in the IDBridge secret vault.

.DESCRIPTION
The way to populate the IDBridge vault. Encrypts the value and writes it as a JSON envelope
file (<Name>.secret.json) in the vault folder (Paths.VaultRoot, <Root>\Vault). The
encryption provider comes from the Secrets.Provider config value:

  - Cms (default) : Protect-CmsMessage with the Document Encryption certificate named by
                    Secrets.Cms.Thumbprint (or the single 'CN=IDBridge Secrets' certificate).
                    Create the certificate with New-IDBridgeSecretCertificate.
  - DpapiNG : DPAPI-NG; protected to the AD principal named by
                    Secrets.DpapiNG.ProtectionDescriptor (e.g. 'SID=<gMSA SID>') so that
                    principal can read it on any domain-joined host. When no descriptor is
                    configured the secret is protected to the current account only (logged
                    as a warning).
  - AzKeyVault : remote — the secret is written to Azure Key Vault over REST (no local
                    envelope), under the same name. Auth is an Entra app registration with
                    a certificate credential (Secrets.AzKeyVault config block). Key Vault
                    names allow only letters, numbers, and dashes.

The envelope records which provider protected it, so Get-IDBridgeSecret reads any mix of
providers from the same vault. Re-running with the same -Name overwrites the existing value.

Requires an initialized session (Initialize-IDBridge) because it reads config and logs via
Write-Log — the same contract as Get-IDBridgeSecret.

.PARAMETER Name
The secret name to store (e.g. 'ApiKey-Passphrase').

.PARAMETER Secret
The secret value as a SecureString. When omitted (and no -InFile) you are prompted to enter
it (masked).

.PARAMETER InFile
Store the raw content of a file as the secret value — e.g. the Google service-account JSON
key: Set-IDBridgeSecret -Name 'GoogleAuth-ServiceAccount' -InFile C:\path\key.json

.EXAMPLE
Set-IDBridgeSecret -Name 'ApiKey-Passphrase'

.EXAMPLE
Set-IDBridgeSecret -Name 'GoogleAuth-ServiceAccount' -InFile 'C:\IDBridge\Auth\key.json'

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-07-01
#>

function Set-IDBridgeSecret {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^[A-Za-z0-9._-]+$')]
        [string]$Name,

        [Parameter()]
        [System.Security.SecureString]$Secret,

        [Parameter()]
        [string]$InFile
    )

    $IDConfig = Get-IDBridgeConfig
    $secretsConfig = $IDConfig.Secrets

    $Provider = if ($secretsConfig -and $secretsConfig.Provider) { $secretsConfig.Provider } else { 'Cms' }

    # Resolve the value to protect: a file's raw content, the supplied SecureString, or a masked prompt.
    if ($InFile) {
        if (-not (Test-Path $InFile)) { Throw "Set-IDBridgeSecret: file not found: $InFile" }
        $plainValue = Get-Content -Path $InFile -Raw
    }
    else {
        if (-not $Secret) {
            $Secret = Read-Host -Prompt "Enter value for secret '$Name'" -AsSecureString
        }
        $plainValue = ConvertFrom-SecureString -SecureString $Secret -AsPlainText
    }

    # Remote provider: write straight to Azure Key Vault, no local envelope
    if ($Provider -eq 'AzKeyVault') {
        if ($Name -notmatch '^[0-9a-zA-Z-]+$') {
            Throw "Secret name '$Name' is not valid for Azure Key Vault: names allow only letters, numbers, and dashes."
        }
        $context = Get-IDBridgeAzKeyVaultContext

        try {
            $null = Invoke-RestMethod -Headers $context.Headers -Uri "$($context.VaultUri)secrets/${Name}?api-version=$($context.ApiVersion)" -Method Put -Body (@{ value = $plainValue } | ConvertTo-Json) -ContentType 'application/json' -ErrorAction Stop
        }
        catch { Throw "Error storing secret '$Name' in Azure Key Vault '$($context.VaultUri)' ($_). Writes need the app registration to hold a write-capable role (e.g. 'Key Vault Secrets Officer')." }

        Write-Log -Message "Secret: Stored '$Name' in Azure Key Vault '$($context.VaultUri)'." -Level Info
        return
    }

    try {
        switch ($Provider) {
            'Cms' {
                $cert = Resolve-IDBridgeCmsCertificate
                $data = Protect-CmsMessage -To $cert -Content $plainValue -ErrorAction Stop
                $protectedTo = "Thumbprint=$($cert.Thumbprint)"
            }
            'DpapiNG' {
                $descriptor = if ($secretsConfig -and $secretsConfig.DpapiNG) { $secretsConfig.DpapiNG.ProtectionDescriptor } else { $null }
                if (-not $descriptor) {
                    $descriptor = "SID=$([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)"
                    Write-Log -Message "Secret: No DpapiNG ProtectionDescriptor configured; '$Name' will be protected to the current account only." -Level Warn
                }
                Import-IDBridgeDpapiNGType
                $bytes = [System.Text.Encoding]::UTF8.GetBytes($plainValue)
                $data = [System.Convert]::ToBase64String([IDBridge.DpapiNG]::Protect($descriptor, $bytes))
                $protectedTo = $descriptor
            }
            default {
                Throw "Unknown secrets provider '$Provider'. Valid values: Cms, DpapiNG, AzKeyVault."
            }
        }
    }
    catch { Throw "Error protecting secret '$Name' with provider '$Provider': $($_)" }

    $vaultPath = $IDConfig.Paths.VaultRoot
    if (-not (Test-Path $vaultPath)) { New-Item -ItemType Directory -Path $vaultPath -Force | Out-Null }

    $envelope = [ordered]@{
        name        = $Name
        provider    = $Provider
        protectedTo = $protectedTo
        created     = (Get-Date).ToString('o')
        data        = $data
    }
    $envelopePath = Join-Path $vaultPath "$Name.secret.json"
    $envelope | ConvertTo-Json | Set-Content -Path $envelopePath -Encoding utf8

    Write-Log -Message "Secret: Stored '$Name' in vault '$vaultPath' (provider $Provider)." -Level Info
}