Created
October 13, 2020 23:23
-
-
Save jborean93/1ccb41bf63726bde399795be2953a7db to your computer and use it in GitHub Desktop.
Extract SSH keys from ssh-agent for the current user
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Copyright: (c) 2020, Jordan Borean (@jborean93) <jborean93@gmail.com> | |
# MIT License (see LICENSE or https://opensource.org/licenses/MIT) | |
Function Find-InArray { | |
<# | |
.SYNOPSIS | |
Finds the index of a byte[] in a byte[]. | |
#> | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[byte[]] | |
$InputObject, | |
[byte[]] | |
$Data | |
) | |
process { | |
$offset = [Array]::IndexOf($decdata, $offsetKey[0], 0) | |
:offsetFound while ($offset -ne -1) { | |
for ($i = 0; $i -lt $offsetKey.Length; $i++) { | |
$decOffset = $offset + $i | |
if ($decdata.Length -lt $decOffset) { | |
break | |
} | |
if ($decdata[$decOffset] -ne $offsetKey[$i]) { | |
break | |
} | |
if ($i -eq $offsetKey.Length - 1) { | |
$offset = $decOffset + 1 | |
break offsetFound | |
} | |
} | |
$offset = [Array]::IndexOf($decdata, $offsetKey[0], $offset + 1) | |
} | |
$offset | |
} | |
} | |
Function ConvertTo-ASN1 { | |
<# | |
.SYNOPSIS | |
Very basic ASN1 encoder, can probably be more efficient but going for readability. | |
#> | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory=$true)] | |
[ValidateSet('Universal', 'Application', 'ContextSpecific', 'Private')] | |
[String] | |
$Class, | |
[Switch] | |
$IsConstructed, | |
[int] | |
$Tag, | |
[byte[]] | |
$Data | |
) | |
$classInt = switch ($Class) { | |
Universal { 0 } | |
Application { 1 } | |
ContextSpecific { 2 } | |
Private { 4 } | |
} | |
$asn1 = [Collections.Generic.List[byte]]@(($classInt -shl 6) -bor ([int]$IsConstructed.IsPresent -shl 5)) | |
if ($Tag -lt 31) { | |
$asn1[0] = $asn1[0] -bor $Tag | |
} | |
else { | |
# Set the first 5 bits of the first octet to 1 then encode the tag number into the subsequent octets. | |
$asn1[0] = $asn1[0] -bor 31 | |
$tagOctets = [Collections.Generic.List[byte]]@() | |
while ($Tag) { | |
$value = $Tag -band 0x7F | |
# If not processing the first tag octet we must set the most significant bit | |
if ($tagOctets.Count) { | |
$value = $value -bor 0x80 | |
} | |
$tagOctets.Add($value) | |
$Tag = $tag -shr 7 | |
} | |
$tagOctets.Reverse() | |
$asn1.AddRange($tagOctets) | |
} | |
if ($Data.Length -lt 128) { | |
$asn1.Add([Convert]::ToByte($Data.Length)) | |
} | |
else { | |
$Length = $Data.Length | |
$lengthOctets = [Collections.Generic.List[byte]]@() | |
while ($Length) { | |
$lengthOctets.Add($Length -band 0xFF) | |
$Length = $Length -shr 8 | |
} | |
$lengthOctets.Reverse() | |
$asn1.Add($lengthOctets.Count -bor 0x80) | |
$asn1.AddRange($lengthOctets) | |
} | |
$asn1.AddRange($Data) | |
,$asn1 | |
} | |
Function Get-SSHAgentKey { | |
<# | |
.SYNOPSIS | |
Gets the keys from ssh-agent for the current user. | |
.DESCRIPTION | |
This extracts the SSH keys from ssh-agent stored for the current user. Currently onlt tested with RSA keys so may | |
require more work for other types of keys. | |
.EXAMPLE | |
Get-SSHAgentKey | |
.EXAMPLE Recreate id_rsa from an ssh-agent key | |
Get-SSHAgentKey | ForEach-Object { | |
$name = Split-Path $_.Comment -Leaf | |
Set-Content -Path $name -Value $_.Private -Encoding ASCII | |
Set-Content -Path "$name.pub" -Value $_.Public -Encoding ASCII | |
} | |
.NOTES | |
Inspired by https://github.com/ropnop/windows_sshagent_extract but as a pure PowerShell function. | |
#> | |
[CmdletBinding()] | |
param () | |
$path = "HKCU:\Software\OpenSSH\Agent\Keys\" | |
Get-ChildItem -LiteralPath $path | Get-ItemProperty | ForEach-Object { | |
$comment = [System.Text.Encoding]::ASCII.GetString($_.comment) | |
Write-Verbose -Message "Pulling key: $comment" | |
$encdata = $_.'(default)' | |
$decdata = [Security.Cryptography.ProtectedData]::Unprotect($encdata, $null, 'CurrentUser') | |
$b64key = [System.Convert]::ToBase64String($decdata) | |
$offsetKey = [Text.Encoding]::ASCII.GetBytes('ssh-rsa') | |
$offset = ,$decdata | Find-InArray -Data $offsetKey | |
if ($offset -eq -1) { | |
Write-Warning -Message "Private key data is not in expected format, cannot parse" | |
return | |
} | |
$readBitUInt32 = { | |
param ([byte[]]$Data, [int]$Offset) | |
$lengthBytes = $Data[$Offset..($Offset + 3)] | |
[Array]::Reverse($lengthBytes) | |
$length = [BitConverter]::ToUInt32($lengthBytes, 0) | |
$offset += 4 | |
$length, $offset, ($offset + $length) | |
} | |
$bigInt = { | |
param ([byte[]]$Data, [int]$Length, [int]$Offset) | |
# Integer values in the private key as big endian numbers larger than Int64. We need to reverse the | |
# bytes and use a BigInteger to get a numeric representation. | |
$bytes = $Data[$Offset..($Offset + $Length - 1)] | |
[Array]::Reverse($bytes) | |
New-Object -TypeName Numerics.BigInteger -ArgumentList @(,$bytes) | |
} | |
$nLength, $nOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$eLength, $eOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$dLength, $dOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$cLength, $cOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$pLength, $pOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$qLength, $qOffset, $offset = .$readBitUInt32 -Data $decdata -Offset $offset | |
$dInt = .$bigInt -Data $decdata -Length $dLength -Offset $dOffset | |
$pInt = .$bigInt -Data $decdata -Length $pLength -Offset $pOffset | |
$qInt = .$bigInt -Data $decdata -Length $qLength -Offset $qOffset | |
$e1Int = $dInt % ($pInt - 1) | |
$e1 = $e1Int.ToByteArray($false, $true) | |
$e2Int = $dInt % ($qInt - 1) | |
$e2 = $e2Int.ToByteArray($false, $true) | |
$integerParams = @{ | |
Class = 'Universal' | |
Tag = 2 | |
} | |
$version = ConvertTo-ASN1 @integerParams -Data 0 | |
$n = ConvertTo-ASN1 @integerParams -Data ($decdata[$nOffset..($nOffset + $nLength - 1)]) | |
$e = ConvertTo-ASN1 @integerParams -Data ($decdata[$eOffset..($eOffset + $eLength - 1)]) | |
$d = ConvertTo-ASN1 @integerParams -Data ($decdata[$dOffset..($dOffset + $dLength - 1)]) | |
$p = ConvertTo-ASN1 @integerParams -Data ($decdata[$pOffset..($pOffset + $pLength - 1)]) | |
$q = ConvertTo-ASN1 @integerParams -Data ($decdata[$qOffset..($qOffset + $qLength - 1)]) | |
$e1 = ConvertTo-ASN1 @integerParams -Data $e1Int.ToByteArray($false, $true) | |
$e2 = ConvertTo-ASN1 @integerParams -Data $e2Int.ToByteArray($false, $true) | |
$c = ConvertTo-ASN1 @integerParams -Data ($decdata[$cOffset..($cOffset + $cLength - 1)]) | |
$sequenceBytes = [Collections.Generic.List[byte]]@() | |
$version, $n, $e, $d, $p, $q, $e1, $e2, $c | ForEach-Object { $sequenceBytes.AddRange($_) } | |
$keyBytes = ConvertTo-ASN1 -Class Universal -IsConstructed -Tag 16 -Data $sequenceBytes | |
$keyB64 = [Convert]::ToBase64String($keyBytes) -split '(.{64})' | Where-Object { $_ } | |
$nl = [Environment]::NewLine | |
$private = "-----BEGIN RSA PRIVATE KEY-----$nl$($keyB64 -join $nl)$nl-----END RSA PRIVATE KEY-----" | |
[PSCustomObject]@{ | |
Comment = $comment | |
Public = "ssh-rsa $([System.Convert]::ToBase64String($_.pub))" | |
PrivateRaw = $b64key | |
Private = $private | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment