436 lines
13 KiB
PowerShell
436 lines
13 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Adds SSH keys from Vaultwarden to the SSH agent.
|
|
.DESCRIPTION
|
|
Retrieves SSH keys stored in Vaultwarden and adds them to the running SSH agent.
|
|
.PARAMETER Debug
|
|
Enables debug output.
|
|
.PARAMETER FolderName
|
|
The name of the Vaultwarden folder containing SSH keys. Defaults to "ssh-agent".
|
|
.PARAMETER ClearSession
|
|
Removes the stored Vaultwarden session from the Windows Credential Manager.
|
|
.EXAMPLE
|
|
.\Vaultwarden_ssh-agent.ps1
|
|
Imports all SSH keys from Vaultwarden to the SSH agent.
|
|
.EXAMPLE
|
|
.\Vaultwarden_ssh-agent.ps1 -FolderName "my-ssh-keys"
|
|
Imports SSH keys from a custom folder name.
|
|
.EXAMPLE
|
|
.\Vaultwarden_ssh-agent.ps1 -ClearSession
|
|
Removes the stored Vaultwarden session from the Windows Credential Manager.
|
|
#>
|
|
|
|
param(
|
|
[switch]$Debug,
|
|
[string]$FolderName = "ssh-agent",
|
|
[switch]$ClearSession
|
|
)
|
|
|
|
# Set debug preference
|
|
if ($Debug) {
|
|
$DebugPreference = 'Continue'
|
|
} else {
|
|
$DebugPreference = 'SilentlyContinue'
|
|
}
|
|
|
|
# Add CredManager type definition at script start
|
|
Add-Type -TypeDefinition @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
public class CredManager {
|
|
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
public static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr);
|
|
|
|
[DllImport("advapi32.dll", SetLastError=true)]
|
|
public static extern void CredFree(IntPtr buffer);
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
|
|
public struct CREDENTIAL {
|
|
public int Flags;
|
|
public int Type;
|
|
public string TargetName;
|
|
public string Comment;
|
|
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
public int CredentialBlobSize;
|
|
public IntPtr CredentialBlob;
|
|
public int Persist;
|
|
public int AttributeCount;
|
|
public IntPtr Attributes;
|
|
public string TargetAlias;
|
|
public string UserName;
|
|
}
|
|
}
|
|
"@
|
|
|
|
function Clear-BWSession {
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
Write-Debug "Removing stored session from Windows Credential Manager"
|
|
& cmdkey /delete:"Vaultwarden_Session" | Out-Null
|
|
}
|
|
|
|
function Test-Prerequisites {
|
|
# Check if ssh-add exists
|
|
if (-not (Get-Command "ssh-add" -ErrorAction SilentlyContinue)) {
|
|
throw "ssh-add command not found. Please ensure OpenSSH is installed."
|
|
}
|
|
|
|
# Check if SSH agent is running
|
|
$process = Get-Process ssh-agent -ErrorAction SilentlyContinue
|
|
if (-not $process) {
|
|
throw "SSH agent is not running. Please start the SSH agent service."
|
|
}
|
|
|
|
# Check if bw CLI is available and version
|
|
try {
|
|
$bwVersion = & bw --version
|
|
if ($bwVersion -match '(\d{4})\.(\d{1,2})' -and
|
|
("$($matches[1])$($matches[2].PadLeft(2,'0'))" -lt "202412")) {
|
|
throw "Bitwarden CLI version $bwVersion is not supported. Please upgrade to version 2024.12.0 or above to use SSH key features."
|
|
}
|
|
} catch {
|
|
throw "Bitwarden CLI not found or version check failed. Please install Bitwarden CLI 2024.12.0 or above."
|
|
}
|
|
}
|
|
|
|
function Test-VaultwardenConfig {
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
$ConfigPath = "$env:APPDATA\Bitwarden CLI\data.json"
|
|
if (Test-Path $ConfigPath) {
|
|
$Config = Get-Content $ConfigPath -Raw | ConvertFrom-Json
|
|
if ($Config.global_environment_environment.region -eq "Self-hosted" -and
|
|
$Config.global_environment_environment.urls.base) {
|
|
Write-Debug "Vaultwarden server is configured: $($Config.global_environment_environment.urls.base)"
|
|
return
|
|
}
|
|
}
|
|
|
|
Write-Host "Vaultwarden server is not configured. Please enter the server URL:"
|
|
$ServerUrl = Read-Host
|
|
& bw config server $ServerUrl
|
|
Write-Host "" # Add line break after failed attempt
|
|
Write-Host "Vaultwarden server configured to: $ServerUrl"
|
|
}
|
|
|
|
function Get-BWSession {
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
Write-Debug "Retrieving session from Windows Credential Manager"
|
|
|
|
$CredPtr = [IntPtr]::Zero
|
|
$Result = [CredManager]::CredRead("Vaultwarden_Session", 1, 0, [ref]$CredPtr)
|
|
|
|
if ($Result) {
|
|
try {
|
|
$Cred = [System.Runtime.InteropServices.Marshal]::PtrToStructure($CredPtr, [Type][CredManager+CREDENTIAL])
|
|
|
|
if (-not $Cred.CredentialBlobSize) {
|
|
Write-Debug "No stored session found"
|
|
return $null
|
|
}
|
|
|
|
$SecretBytes = New-Object byte[] $Cred.CredentialBlobSize
|
|
[System.Runtime.InteropServices.Marshal]::Copy($Cred.CredentialBlob, $SecretBytes, 0, $Cred.CredentialBlobSize)
|
|
$Session = [System.Text.Encoding]::Unicode.GetString($SecretBytes)
|
|
|
|
# Check session status
|
|
Write-Debug "Stored session found"
|
|
# bw status is always locked when using --session
|
|
# https://github.com/bitwarden/clients/issues/9254
|
|
[Environment]::SetEnvironmentVariable("BW_SESSION", $Session, [System.EnvironmentVariableTarget]::Process)
|
|
$StatusResult = & bw status 2>&1
|
|
[Environment]::SetEnvironmentVariable("BW_SESSION", $null, [System.EnvironmentVariableTarget]::Process)
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$Status = $StatusResult | ConvertFrom-Json
|
|
if ($Status.status -eq "unlocked") {
|
|
Write-Debug "Stored session is valid"
|
|
Write-Host "Using stored session"
|
|
return $Session
|
|
}
|
|
}
|
|
|
|
Write-Debug "Stored session is invalid, removing"
|
|
& cmdkey /delete:"Vaultwarden_Session" | Out-Null
|
|
}
|
|
finally {
|
|
[CredManager]::CredFree($CredPtr)
|
|
if ($Session) {
|
|
Clear-SensitiveData $Session
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host "Getting new session"
|
|
|
|
# Check current login status
|
|
Write-Debug "Checking login status"
|
|
$Status = & bw status | ConvertFrom-Json
|
|
|
|
if ($Status.status -eq "unauthenticated") {
|
|
Write-Debug "Not logged in, attempting API key login"
|
|
& bw login --apikey
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Login failed"
|
|
}
|
|
}
|
|
else {
|
|
Write-Debug "Already logged in as $($Status.userEmail)"
|
|
}
|
|
|
|
Write-Debug "Unlocking vault"
|
|
$NewSession = & bw unlock --raw
|
|
if (-not $NewSession) {
|
|
throw "Failed to unlock vault"
|
|
}
|
|
|
|
# Store session in Windows Credential Manager
|
|
Write-Debug "Storing session in Windows Credential Manager"
|
|
$Result = & cmdkey /generic:"Vaultwarden_Session" /user:$($Status.userEmail) /pass:$NewSession
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to store session: $Result"
|
|
}
|
|
|
|
Write-Host "New session established"
|
|
return $NewSession
|
|
}
|
|
|
|
function Clear-SensitiveData {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[System.Object]$Variable
|
|
)
|
|
|
|
if ($Variable -is [SecureString]) {
|
|
$Variable.Dispose()
|
|
}
|
|
elseif ($Variable -is [string]) {
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR(
|
|
[Runtime.InteropServices.Marshal]::StringToBSTR($Variable)
|
|
)
|
|
}
|
|
}
|
|
|
|
function Test-SSHKey {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$KeyContent,
|
|
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateSet('Public', 'Private')]
|
|
[string]$KeyType
|
|
)
|
|
|
|
$Patterns = @{
|
|
Public = '^(ssh-rsa|ssh-dss|ssh-ed25519|ecdsa-sha2-nistp\d+)\s+[A-Za-z0-9+/=]+\s*.*$'
|
|
Private = '(?sm)^-----BEGIN\s+(OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----.*-----END\s+(OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----$'
|
|
}
|
|
|
|
return $KeyContent -match $Patterns[$KeyType]
|
|
}
|
|
|
|
function Get-FolderId {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Session
|
|
)
|
|
|
|
Write-Debug "Getting folder: $FolderName"
|
|
$Folders = & bw list folders --session $Session --search $FolderName | ConvertFrom-Json
|
|
$Folder = $Folders | Where-Object { $_.name -eq $FolderName }
|
|
|
|
if (-not $Folder) {
|
|
throw "'$FolderName' folder not found"
|
|
}
|
|
return $Folder.id
|
|
}
|
|
|
|
function Get-FolderItems {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Session,
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$FolderId
|
|
)
|
|
|
|
Write-Debug "Getting items from folder: $FolderId"
|
|
# Add filter for SSH key type (type=5)
|
|
$Items = & bw list items --session $Session --folderid $FolderId | ConvertFrom-Json
|
|
$SshKeyItems = $Items | Where-Object { $_.type -eq 5 }
|
|
Write-Debug "Found $($SshKeyItems.Count) SSH key items"
|
|
return $SshKeyItems
|
|
}
|
|
|
|
function Get-PrivatePublicKey {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[string]$Session,
|
|
[Parameter(Mandatory=$true)]
|
|
[PSObject]$Item
|
|
)
|
|
|
|
try {
|
|
if ($Item.type -ne 5) {
|
|
throw "Item is not an SSH key type"
|
|
}
|
|
|
|
# Get public key from the sshKey property
|
|
$PublicKey = $Item.sshKey.publicKey
|
|
|
|
if (-not (Test-SSHKey -KeyContent $PublicKey -KeyType 'Public')) {
|
|
throw "Invalid public key format"
|
|
}
|
|
|
|
Write-Debug "Valid public key found for: $($Item.name)"
|
|
|
|
# Get private key from the sshKey property
|
|
$PrivateKey = $Item.sshKey.privateKey
|
|
|
|
if (-not (Test-SSHKey -KeyContent $PrivateKey -KeyType 'Private')) {
|
|
throw "Invalid private key format"
|
|
}
|
|
|
|
Write-Debug "Valid private key found for: $($Item.name)"
|
|
|
|
return @{
|
|
PublicKey = $PublicKey
|
|
PrivateKey = ConvertTo-SecureString $PrivateKey -AsPlainText -Force
|
|
Name = $Item.name
|
|
}
|
|
}
|
|
finally {
|
|
if ($PrivateKey) {
|
|
Clear-SensitiveData $PrivateKey
|
|
}
|
|
}
|
|
}
|
|
|
|
function Add-PrivateKeyToSSHAgent {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)]
|
|
[ValidateNotNull()]
|
|
[PSCustomObject]$SSHKey
|
|
)
|
|
|
|
try {
|
|
Write-Debug "Adding key: $($SSHKey.Name)"
|
|
|
|
$PrivateKeyPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
|
|
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($SSHKey.PrivateKey)
|
|
)
|
|
|
|
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
|
|
$ProcessInfo.FileName = "ssh-add"
|
|
$ProcessInfo.Arguments = "-"
|
|
$ProcessInfo.RedirectStandardInput = $true
|
|
$ProcessInfo.RedirectStandardOutput = $true
|
|
$ProcessInfo.RedirectStandardError = $true
|
|
$ProcessInfo.UseShellExecute = $false
|
|
|
|
$Process = New-Object System.Diagnostics.Process
|
|
$Process.StartInfo = $ProcessInfo
|
|
|
|
$Process.Start() | Out-Null
|
|
$Process.StandardInput.WriteLine($PrivateKeyPlain)
|
|
$Process.StandardInput.Close()
|
|
$Process.WaitForExit()
|
|
|
|
if ($Process.ExitCode -eq 0) {
|
|
Write-Debug "Key added successfully: $($SSHKey.Name)"
|
|
return $true
|
|
} else {
|
|
Write-Warning "Failed to add key: $($SSHKey.Name)"
|
|
return $false
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Exception adding key $($SSHKey.Name): $_"
|
|
return $false
|
|
}
|
|
finally {
|
|
if ($PrivateKeyPlain) {
|
|
Clear-SensitiveData $PrivateKeyPlain
|
|
}
|
|
if ($Process) {
|
|
$Process.Dispose()
|
|
}
|
|
}
|
|
}
|
|
|
|
# Main script execution
|
|
try {
|
|
if ($ClearSession) {
|
|
Clear-BWSession
|
|
Write-Host "Stored session has been cleared"
|
|
return
|
|
}
|
|
|
|
Test-Prerequisites
|
|
|
|
Write-Debug "Checking Vaultwarden configuration"
|
|
Test-VaultwardenConfig
|
|
|
|
# Get session
|
|
$Session = Get-BWSession
|
|
|
|
# Sync vault
|
|
Write-Debug "Syncing vault"
|
|
& bw sync --session $Session --quiet
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to sync vault"
|
|
}
|
|
|
|
$FolderId = Get-FolderId -Session $Session
|
|
$Items = Get-FolderItems -Session $Session -FolderId $FolderId
|
|
|
|
$Results = @{
|
|
Success = @()
|
|
Failed = @()
|
|
}
|
|
|
|
Write-Host "Adding SSH keys to agent..."
|
|
foreach ($Item in $Items) {
|
|
try {
|
|
$SSHKey = Get-PrivatePublicKey -Session $Session -Item $Item
|
|
if (Add-PrivateKeyToSSHAgent -SSHKey $SSHKey) {
|
|
$Results.Success += $Item.name
|
|
} else {
|
|
$Results.Failed += $Item.name
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "Failed to process $($Item.name): $_"
|
|
$Results.Failed += $Item.name
|
|
}
|
|
}
|
|
|
|
# Report results
|
|
Write-Host "`nResults:"
|
|
Write-Host "Successfully added keys: $($Results.Success.Count)"
|
|
$Results.Success | ForEach-Object { Write-Host " - $_" }
|
|
|
|
if ($Results.Failed.Count -gt 0) {
|
|
Write-Host "`nFailed to add keys: $($Results.Failed.Count)"
|
|
$Results.Failed | ForEach-Object { Write-Host " - $_" }
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Critical error: $_"
|
|
exit 1
|
|
}
|
|
finally {
|
|
if ($Session) {
|
|
Clear-SensitiveData $Session
|
|
}
|
|
}
|