126 lines
6.0 KiB
PowerShell
126 lines
6.0 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[ValidateSet('Codex', 'Claude')][string]$Client,
|
|
[string]$Key,
|
|
[Parameter(Position = 2)][string]$FilePath,
|
|
[Parameter(Position = 3, ValueFromRemainingArguments = $true)][string[]]$ArgumentList
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Assert-CredentialKey {
|
|
param([string]$Value)
|
|
if ($Value -notmatch '^[A-Za-z_][A-Za-z0-9_]{0,127}$') { throw 'Credential key must be an environment-variable-compatible name.' }
|
|
}
|
|
|
|
function Get-CredentialTarget {
|
|
param([string]$Client, [string]$Key)
|
|
Assert-CredentialKey $Key
|
|
return "MyAiConfiguration/$($Client.ToLowerInvariant())/$Key"
|
|
}
|
|
|
|
function Initialize-NativeCredentialApi {
|
|
if ('MyAiConfiguration.NativeCredential' -as [type]) { return }
|
|
Add-Type -TypeDefinition @'
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
namespace MyAiConfiguration {
|
|
public static class NativeCredential {
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
public struct CREDENTIAL {
|
|
public UInt32 Flags;
|
|
public UInt32 Type;
|
|
public IntPtr TargetName;
|
|
public IntPtr Comment;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
public UInt32 CredentialBlobSize;
|
|
public IntPtr CredentialBlob;
|
|
public UInt32 Persist;
|
|
public UInt32 AttributeCount;
|
|
public IntPtr Attributes;
|
|
public IntPtr TargetAlias;
|
|
public IntPtr UserName;
|
|
}
|
|
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
public static extern bool CredWrite(ref CREDENTIAL credential, UInt32 flags);
|
|
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
public static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
[DllImport("Advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
public static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
[DllImport("Advapi32.dll", SetLastError = true)]
|
|
public static extern void CredFree(IntPtr buffer);
|
|
}
|
|
}
|
|
'@
|
|
}
|
|
|
|
function Set-ManagedCredential {
|
|
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key, [securestring]$Value)
|
|
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
|
Initialize-NativeCredentialApi
|
|
$plain = [System.Net.NetworkCredential]::new('', $Value).Password
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes($plain)
|
|
if ($bytes.Length -eq 0 -or $bytes.Length -gt 2560) { throw 'Credential value must contain between 1 and 2560 UTF-8 bytes.' }
|
|
$native = New-Object MyAiConfiguration.NativeCredential+CREDENTIAL
|
|
$native.Type = 1
|
|
$native.Persist = 2
|
|
$native.TargetName = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni((Get-CredentialTarget $Client $Key))
|
|
$native.UserName = [Runtime.InteropServices.Marshal]::StringToCoTaskMemUni($Key)
|
|
$native.CredentialBlobSize = $bytes.Length
|
|
$native.CredentialBlob = [Runtime.InteropServices.Marshal]::AllocCoTaskMem($bytes.Length)
|
|
try {
|
|
[Runtime.InteropServices.Marshal]::Copy($bytes, 0, $native.CredentialBlob, $bytes.Length)
|
|
if (-not [MyAiConfiguration.NativeCredential]::CredWrite([ref]$native, 0)) { throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()) }
|
|
} finally {
|
|
[Array]::Clear($bytes, 0, $bytes.Length)
|
|
$plain = $null
|
|
if ($native.TargetName -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.TargetName) }
|
|
if ($native.UserName -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.UserName) }
|
|
if ($native.CredentialBlob -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeCoTaskMem($native.CredentialBlob) }
|
|
}
|
|
}
|
|
|
|
function Get-ManagedCredential {
|
|
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key)
|
|
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
|
Initialize-NativeCredentialApi
|
|
$pointer = [IntPtr]::Zero
|
|
if (-not [MyAiConfiguration.NativeCredential]::CredRead((Get-CredentialTarget $Client $Key), 1, 0, [ref]$pointer)) { throw 'Credential was not found.' }
|
|
try {
|
|
$native = [Runtime.InteropServices.Marshal]::PtrToStructure($pointer, [type]'MyAiConfiguration.NativeCredential+CREDENTIAL')
|
|
$bytes = New-Object byte[] $native.CredentialBlobSize
|
|
[Runtime.InteropServices.Marshal]::Copy($native.CredentialBlob, $bytes, 0, $bytes.Length)
|
|
try { return [Text.Encoding]::UTF8.GetString($bytes) } finally { [Array]::Clear($bytes, 0, $bytes.Length) }
|
|
} finally {
|
|
[MyAiConfiguration.NativeCredential]::CredFree($pointer)
|
|
}
|
|
}
|
|
|
|
function Remove-ManagedCredential {
|
|
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key)
|
|
if (-not $IsWindows -and $PSVersionTable.PSVersion.Major -ge 6) { throw 'The OS credential store is currently supported on Windows only.' }
|
|
Initialize-NativeCredentialApi
|
|
if (-not [MyAiConfiguration.NativeCredential]::CredDelete((Get-CredentialTarget $Client $Key), 1, 0)) { throw [ComponentModel.Win32Exception]::new([Runtime.InteropServices.Marshal]::GetLastWin32Error()) }
|
|
}
|
|
|
|
function Invoke-WithManagedCredential {
|
|
param([ValidateSet('Codex', 'Claude')][string]$Client, [string]$Key, [string]$FilePath, [string[]]$ArgumentList)
|
|
Assert-CredentialKey $Key
|
|
if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { throw 'Child command was not found.' }
|
|
$prior = [Environment]::GetEnvironmentVariable($Key, 'Process')
|
|
$value = Get-ManagedCredential $Client $Key
|
|
try {
|
|
[Environment]::SetEnvironmentVariable($Key, $value, 'Process')
|
|
& $FilePath @ArgumentList
|
|
return $LASTEXITCODE
|
|
} finally {
|
|
[Environment]::SetEnvironmentVariable($Key, $prior, 'Process')
|
|
$value = $null
|
|
}
|
|
}
|
|
|
|
if ($FilePath) {
|
|
if (-not $Client -or -not $Key) { throw 'Client and key are required.' }
|
|
exit (Invoke-WithManagedCredential $Client $Key $FilePath $ArgumentList)
|
|
}
|