Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created October 13, 2020 23:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jborean93/1ccb41bf63726bde399795be2953a7db to your computer and use it in GitHub Desktop.
Save jborean93/1ccb41bf63726bde399795be2953a7db to your computer and use it in GitHub Desktop.
Extract SSH keys from ssh-agent for the current user
# 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