-
-
Save arnehormann/9486751 to your computer and use it in GitHub Desktop.
package main | |
import ( | |
"bytes" | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/hmac" | |
"crypto/rsa" | |
"crypto/sha1" | |
"crypto/x509" | |
"encoding/base64" | |
"encoding/binary" | |
"encoding/hex" | |
"encoding/pem" | |
"errors" | |
"flag" | |
"hash" | |
"io/ioutil" | |
"math/big" | |
"os" | |
"strconv" | |
) | |
/* | |
load | |
http://www.nanobit.net/doxy/putty_suite/SSH_8H.html#a3af5f5142bd6b680556426910dae7c09 | |
with alg rsa2 | |
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a2bb0abdf389a18bd3b0c9f0cb171692f | |
using for blobs | |
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C_source.html#l00517 | |
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a3ad0809b62fc0d0c99f24f58bc68f97e | |
using for createkey | |
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a889e2bb4dc454d99686e367259f99cdd | |
*/ | |
const ( | |
aesBlockSize = aes.BlockSize | |
aes256KeySize = 32 | |
plainpk = "plain" | |
encryptedpk = "aes256-cbc" | |
) | |
var ( | |
// errors | |
errKeyShort = errors.New("key is not in putty ssh2:rsa format, it is too short") | |
errKeyUnknown = errors.New("key is not in putty ssh2:rsa format") | |
errNilKey = errors.New("key must not be nil") | |
// header values and internal strings | |
ppkKeytype = []byte("ssh-rsa") | |
ppkEncrypted = []byte(encryptedpk) | |
ppkPlain = []byte(plainpk) | |
ppkHashPrefix = []byte("putty-private-key-file-mac-key") | |
// header keys | |
ppkKeyFile = []byte("PuTTY-User-Key-File-2") | |
ppkEncryption = []byte("Encryption") | |
ppkComment = []byte("Comment") | |
ppkPublic = []byte("Public-Lines") | |
ppkPrivate = []byte("Private-Lines") | |
ppkMac = []byte("Private-MAC") | |
// aggregated for faster copying | |
ppkStartPrefix = []byte("" + | |
string(ppkKeyFile) + ": " + string(ppkKeytype) + "\n" + | |
string(ppkEncryption) + ": ") | |
ppkStartPostfix = []byte("\n" + string(ppkComment) + ": ") | |
ppkStartEncrypted = []byte("" + | |
string(ppkStartPrefix) + | |
string(ppkEncrypted) + | |
string(ppkStartPostfix)) | |
ppkStartPlain = []byte("" + | |
string(ppkStartPrefix) + | |
string(ppkPlain) + | |
string(ppkStartPostfix)) | |
ppkSectionPub = []byte(string(ppkPublic) + ": ") | |
ppkSectionPriv = []byte(string(ppkPrivate) + ": ") | |
ppkSectionMac = []byte(string(ppkMac) + ": ") | |
ppkMinLen = 0 + | |
len(ppkStartPlain) + 1 + | |
len(ppkSectionPub) + 1 + 1 + | |
len(ppkSectionPriv) + 1 + 1 + | |
len(ppkSectionMac) + 2*sha1.Size + 1 | |
) | |
func appendBytes(dst []byte, value []byte) []byte { | |
offset := len(dst) | |
dst = append(dst, 0, 0, 0, 0) | |
binary.BigEndian.PutUint32(dst[offset:offset+4], uint32(len(value))) | |
return append(dst, value...) | |
} | |
func appendMpint(dst []byte, value *big.Int) []byte { | |
offset := len(dst) | |
dst = append(dst, 0, 0, 0, 0) | |
data := value.Bytes() | |
datalen := uint32(len(data)) | |
if data[0]&0x80 != 0 { | |
// handle a set first bit: prefix with 0 byte | |
datalen++ | |
dst = append(dst, 0) | |
} | |
binary.BigEndian.PutUint32(dst[offset:offset+4], datalen) | |
dst = append(dst, data...) | |
// negative values start with a 1-bit | |
if value.Sign() < 0 { | |
dst[offset+4] |= 0x80 | |
} | |
return dst | |
} | |
func appendUint32(dst []byte, value uint32) []byte { | |
const maxlen = 4 + 1 + 4 | |
var datalen int | |
var ext [maxlen]byte | |
switch { | |
case value > 0xffffff: | |
datalen = 4 | |
case value > 0xffff: | |
datalen = 3 | |
case value > 0xff: | |
datalen = 2 | |
default: | |
datalen = 1 | |
} | |
// data | |
binary.BigEndian.PutUint32(ext[4+1:], value) | |
if ext[maxlen-datalen]&0x80 != 0 { | |
datalen++ | |
} | |
// shrink to start of length | |
data := ext[maxlen-datalen-4:] | |
// length of data | |
binary.BigEndian.PutUint32(data[:4], uint32(datalen)) | |
return append(dst, data...) | |
} | |
func base64BlockLines(data []byte) int { | |
return (len(data) + 47) / 48 | |
} | |
func appendBase64Block(dst, data []byte) []byte { | |
buffer := [65]byte{} | |
buffer[64] = '\n' | |
b64 := buffer[:64] | |
line := buffer[:] | |
for ; len(data) > 48; data = data[48:] { | |
base64.StdEncoding.Encode(b64, data[:48]) | |
dst = append(dst, line...) | |
} | |
if len(data) == 0 { | |
return dst | |
} | |
chars64 := 4 * ((len(data) + 2) / 3) | |
base64.StdEncoding.Encode(line[:chars64], data) | |
line[chars64] = '\n' | |
return append(dst, line[:chars64+1]...) | |
} | |
// hashPrefix hashes data into h with | |
// a uint32 big endian length prefix | |
func hashPrefixed(h hash.Hash, data []byte) { | |
var buffer = []byte{0, 0, 0, 0} | |
binary.BigEndian.PutUint32(buffer, uint32(len(data))) | |
h.Write(buffer) | |
h.Write(data) | |
} | |
// cryptgen is a decrypter or encrypter generation function from | |
// cipher. | |
type cryptgen func(b cipher.Block, iv []byte) cipher.BlockMode | |
// cryptPrivate en- or decrypts data from src into dst. | |
// src and dst may overlap. | |
func cryptPrivate(dst, src []byte, password string, sha hash.Hash, crypt cryptgen) { | |
var twosha1 [2 * sha1.Size]byte | |
// create weird putty cipher key | |
passBytes := make([]byte, 4+len(password)) | |
// round 1: base is 0 0 0 0 <password> | |
copy(passBytes[4:], password) | |
sha.Reset() | |
sha.Write(passBytes) | |
sha.Sum(twosha1[0:0]) | |
// round 2: base is 0 0 0 1 <password> | |
passBytes[3] = 1 | |
sha.Reset() | |
sha.Write(passBytes) | |
sha.Sum(twosha1[sha1.Size:sha1.Size]) | |
// encrypt src with aes256-cbc | |
block, _ := aes.NewCipher(twosha1[:aes256KeySize]) | |
iv0 := [aesBlockSize]byte{} | |
crypter := crypt(block, iv0[:]) | |
crypter.CryptBlocks(dst, src) | |
} | |
// nextLine returns the index of the next non '\r' / '\n' byte | |
// after offset. | |
func nextLine(src []byte, offset int) int { | |
for i, b := range src[next:] { | |
switch b { | |
case '\r', '\n': | |
next++ | |
default: | |
return offset + i | |
} | |
} | |
return -1 | |
} | |
// parseLine parses a single header starting at src[offset:]. | |
// It returns the value and the next offset. | |
func parseLine(key, src []byte, offset int) (value []byte, next int) { | |
// Header format: | |
// ([^:\r\n]*) ": " ([^\r\n]*) | |
if len(src) < offset+len(key)+2 || | |
!bytes.Equal(key, src[offset:offset+len(key)]) { | |
return | |
} | |
offset += len(key) | |
value = src[offset:] | |
if string(value[:2]) != ": " { | |
return | |
} | |
offset += 2 | |
value = src[offset:] | |
nextterm := bytes.IndexByte(value, '\n') | |
if nextterm > 0 { | |
nextrl := bytes.IndexByte(value[:nextterm], '\r') | |
if nextrl > 0 { | |
// ending with '\r' instead of '\n' | |
nextterm = nextrl | |
} | |
} | |
value = src[offset:nextterm] | |
next = nextLine(src, offset+len(value)) | |
return | |
} | |
// parseBlob64 parses the base64 encoded blob in src[offset:] of 64 byte long '\n' terminated lines | |
// into dst and returns a slice of dst, the next offset after the read blob. | |
func parseBlob64(dst, src []byte, offset, lines int) (blob []byte, next int, err error) { | |
lastline := offset + (lines-1)*65 | |
end := lastline + bytes.IndexByte(src[lastline:], '\n') | |
if end < lastline { | |
err = errKeyUnknown | |
return | |
} | |
blob64 := src[offset:end] | |
if requiredLen := 6 * ((len(blob64) - numlines) / 8); len(dst) < requiredLen { | |
dst = make([]byte, requiredLen) | |
} | |
n, err := base64.StdEncoding.Decode(dst, blob64) | |
if err != nil { | |
return | |
} | |
blob = dst[:n] | |
next = nextLine(src, end+1) | |
return | |
} | |
// parseMpint parses an multi-precision integer (mpint). | |
// An mpint is prefixed with the length of its data part as a 4 byte big endian number. | |
// The data part is a big endian array of bytes. | |
// If the first bit is set, it is negative. | |
func parseMpint(src []byte, offset int) (*big.Int, int) { | |
if len(src) < offset+5 { | |
return | |
} | |
src = src[offset:] | |
length := binary.BigEndian.Uint32(src[:4]) | |
if len(src) < offset+4+length { | |
return | |
} | |
byte0 := src[4] | |
data := src[4 : 4+length] | |
value := big.NewInt(0) | |
if byte0&0x7f == 0 { | |
data = data[1:] | |
} | |
value.SetBytes(data) | |
if byte0&0x80 != 0 { | |
value = value.Neg(value) | |
} | |
src[4] = byte0 | |
return value, offset + 4 + length | |
} | |
// ParsePpk parses | |
// does not perform key validation | |
func ParsePpk(ppk []byte, password string) (key *rsa.PrivateKey, comment string, err error) { | |
if len(ppk) < ppkMinLen { | |
err = errKeyShort | |
return | |
} | |
// prepare sha1 hmac | |
hasher := sha1.New() | |
hashbuff := make([]byte, hasher.Size()) | |
hasher.Reset() | |
hasher.Write(ppkHashPrefix) | |
hasher.Write([]byte(password)) | |
hashed := hasher.Sum(hashbuff[:0]) | |
sha1hmac := hmac.New(sha1.New, hashed) | |
// parse file | |
var data []byte | |
var offset, next int | |
// file format | |
data, next = parseLine(ppkKeyFile, ppk, 0) | |
if next <= offset || !bytes.Equal(data, ppkKeytype) { | |
err = errKeyUnknown | |
return | |
} | |
hashPrefixed(sha1hmac, data) | |
offset = next | |
// encryption | |
data, next = parseLine(ppkEncryption, ppk, offset) | |
var encrypted bool | |
switch string(data) { | |
default: | |
err = errKeyUnknown | |
return | |
case encryptedpk: | |
encrypted = true | |
case plainpk: | |
// nothing | |
} | |
hashPrefixed(sha1hmac, data) | |
offset = next | |
// comment | |
comm, next := parseLine(ppkComment, ppk, offset) | |
if next <= offset { | |
err = errKeyUnknown | |
return | |
} | |
hashPrefixed(sha1hmac, comm) | |
offset = next | |
// declare blob buffer | |
var blobbuffer [1024]byte | |
var numlines int | |
// public key lines | |
data, next = parseLine(ppkPublic, ppk, offset) | |
if next <= offset { | |
err = errKeyUnknown | |
return | |
} | |
numlines, err = strconv.ParseInt(string(data), 10, 0) | |
if err != nil { | |
err = errKeyUnknown | |
return | |
} | |
offset = next | |
data, next = parseBlob(blobbuffer[:], ppk, offset) | |
if next <= offset { | |
err = errKeyUnknown | |
return | |
} | |
// key values | |
var N, E *big.Int | |
var nextval int | |
N, nextval = parseMpint(data, 0) | |
if nextval <= 0 { | |
err = errKeyUnknown | |
return | |
} | |
E, nextval = parseMpint(data, nextval) | |
if nextval <= 0 { | |
err = errKeyUnknown | |
return | |
} | |
hashPrefixed(sha1hmac, data) | |
offset = next | |
// private key lines | |
data, next = parseLine(ppkPrivate, ppk, offset) | |
if next <= offset { | |
err = errKeyUnknown | |
return | |
} | |
numlines, err = strconv.ParseInt(string(data), 10, 0) | |
if err != nil { | |
err = errKeyUnknown | |
return | |
} | |
offset = next | |
data, next = parseBlob(blobbuffer[:], ppk, offset) | |
if next <= offset { | |
err = errKeyUnknown | |
return | |
} | |
if encrypted { | |
cryptPrivate(data, data, password, hasher, cipher.NewCBCDecrypter) | |
} | |
var D, P, Q *big.Int | |
D, nextval = parseMpint(data, 0) | |
if nextval <= 0 { | |
err = errKeyUnknown | |
return | |
} | |
P, nextval = parseMpint(data, nextval) | |
if nextval <= 0 { | |
err = errKeyUnknown | |
return | |
} | |
Q, nextval = parseMpint(data, nextval) | |
if nextval <= 0 { | |
err = errKeyUnknown | |
return | |
} | |
// ignore Q^-1 mod P and padding | |
hashPrefixed(sha1hmac, data) | |
offset = next | |
// hmac | |
data, next = parseLine(ppkMac, ppk, offset) | |
if next <= offset || next != len(ppk) { | |
err = errKeyUnknown | |
return | |
} | |
mac := hex.Decode(hashbuff[:0], data) | |
hash0 := hashbuff[:] | |
hash1 := sha1hmac.Sum(nil) | |
// no need for constant time / side channel safety, | |
// this is just a key conversion api. | |
if !bytes.Equal(hash0, hash1) { | |
err = errKeyUnknown | |
return | |
} | |
key = &rsa.PrivateKey{ | |
PublicKey: PublicKey{N: N, E: int(E.Int64())}, | |
D: D, | |
Primes: []*big.Int{P, Q}, | |
} | |
comment = string(comm) | |
return | |
} | |
func AppendPpk(dst []byte, key *rsa.PrivateKey, password, comment string) ([]byte, error) { | |
if key == nil { | |
return nil, errNilKey | |
} | |
key.Precompute() | |
blobBuffer := [1024]byte{} | |
twosha1 := [2 * sha1.Size]byte{} | |
hasher := sha1.New() | |
hasher.Reset() | |
hasher.Write(ppkHashPrefix) | |
hasher.Write([]byte(password)) | |
hashed := hasher.Sum(twosha1[:0]) | |
sha1hmac := hmac.New(sha1.New, hashed) | |
hashPrefixed(sha1hmac, ppkKeytype) | |
ppk := dst[:0] | |
// PPK LEAD IN | |
if password == "" { | |
ppk = append(ppk, ppkStartPlain...) | |
hashPrefixed(sha1hmac, ppkPlain) | |
} else { | |
ppk = append(ppk, ppkStartEncrypted...) | |
hashPrefixed(sha1hmac, ppkEncrypted) | |
} | |
// PPK SECTION: COMMENT | |
hashPrefixed(sha1hmac, []byte(comment)) | |
ppk = append(ppk, comment...) | |
ppk = append(ppk, '\n') | |
// PPK SECTION: PUBLIC LINES | |
var blob []byte | |
var bloblines int | |
ppk = append(ppk, ppkSectionPub...) | |
blob = blobBuffer[:0] | |
blob = appendBytes(blob, ppkKeytype) | |
blob = appendUint32(blob, uint32(key.PublicKey.E)) // public exponent | |
blob = appendMpint(blob, key.N) // modulus | |
hashPrefixed(sha1hmac, blob) | |
bloblines = base64BlockLines(blob) | |
ppk = strconv.AppendUint(ppk, uint64(bloblines), 10) | |
ppk = append(ppk, '\n') | |
ppk = appendBase64Block(ppk, blob) | |
// PPK SECTION: PRIVATE LINES | |
ppk = append(ppk, ppkSectionPriv...) | |
blob = blobBuffer[:0] | |
blob = appendMpint(blob, key.D) // private exponent | |
blob = appendMpint(blob, key.Primes[0]) // P | |
blob = appendMpint(blob, key.Primes[1]) // Q | |
blob = appendMpint(blob, key.Precomputed.Qinv) // Q^-1 mod P | |
// if encrypted, pad before MAC | |
if password != "" { | |
// add padding if not multiple of block size | |
bytesMissing := (aesBlockSize - len(blob)%aesBlockSize) % aesBlockSize | |
if bytesMissing > 0 { | |
hasher.Reset() | |
hasher.Write(blob) | |
padding := hasher.Sum(twosha1[:0]) | |
blob = append(blob, padding[:bytesMissing]...) | |
} | |
} | |
hashPrefixed(sha1hmac, blob) | |
bloblines = base64BlockLines(blob) | |
ppk = strconv.AppendUint(ppk, uint64(bloblines), 10) | |
ppk = append(ppk, '\n') | |
// encrypt private blob after MAC | |
if password != "" { | |
cryptPrivate(blob, blob, password, hasher, cipher.NewCBCEncrypter) | |
} | |
ppk = appendBase64Block(ppk, blob) | |
// PPK SECTION: PRIVATE MAC | |
ppk = append(ppk, ppkSectionMac...) | |
// len(sha1store) has the length of hex encoded sha1 | |
offset := len(ppk) | |
ppk = append(ppk, twosha1[:]...) | |
privateMac := sha1hmac.Sum(twosha1[:0]) | |
hex.Encode(ppk[offset:], privateMac) | |
return append(ppk, '\n'), nil | |
} | |
func main() { | |
var comment, passin, passout string | |
flag.StringVar(&comment, "comment", "", "comment for the public key (e.g. user account)") | |
flag.StringVar(&passin, "passin", "", "password to decrypt the pem file") | |
flag.StringVar(&passout, "passout", "", "password to encrypt the ppk file") | |
flag.Parse() | |
pemBlock, err := ioutil.ReadAll(os.Stdin) | |
if err != nil { | |
os.Stderr.WriteString("could not read pem block: " + err.Error()) | |
return | |
} | |
block, _ := pem.Decode(pemBlock) | |
if x509.IsEncryptedPEMBlock(block) { | |
contents, err := x509.DecryptPEMBlock(block, []byte(passin)) | |
if err != nil { | |
os.Stderr.WriteString("could not decrypt pem block: " + err.Error()) | |
return | |
} | |
block.Bytes = contents | |
} | |
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) | |
if err != nil { | |
os.Stderr.WriteString("could not parse private key: " + err.Error()) | |
return | |
} | |
buffer := make([]byte, 2048) | |
ppk, err := AppendPpk(buffer, rsaKey, passout, comment) | |
os.Stdout.Write(ppk) | |
} |
Sources:
http://www.nanobit.net/doxy/putty_suite/SSHAES_8C.html
http://www.nanobit.net/doxy/putty_suite/SSHPUBK_8C.html
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html
http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html
http://www.nanobit.net/doxy/putty_suite/WINPGNT_8C.html
from
- http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html#a5a4664cad6192289e6cd097858730295
- http://www.nanobit.net/doxy/putty_suite/WINPGEN_8C.html#abc0555fc750fe9f486f0b6293e679ae2
- load:
- store:
with alg rsa2
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a2bb0abdf389a18bd3b0c9f0cb171692f
using for blobs
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C_source.html#l00517
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a3ad0809b62fc0d0c99f24f58bc68f97e
using for createkey
http://www.nanobit.net/doxy/putty_suite/SSHRSA_8C.html#a889e2bb4dc454d99686e367259f99cdd
with Bignum:
Bignum voidptr
BignumInt uint32
BignumDblInt uint64
BIGNUM_INT_BITS 32
BIGNUM_INT_BYTES 4
and Bignum format
BignumInt length:num used bytes of data
| BignumInt* data:the number
those have to be prefixed with 0 if the first bit is 1
Found this on google. Tried to compile (go version go1.4.2 darwin/amd64) and get:
❯ go build puttygen.go
# command-line-arguments
./puttygen.go:199: undefined: next
./puttygen.go:202: undefined: next
./puttygen.go:249: undefined: numlines
./puttygen.go:267: not enough arguments to return
./puttygen.go:271: invalid operation: offset + 4 + length (mismatched types int and uint32)
./puttygen.go:272: not enough arguments to return
./puttygen.go:285: invalid operation: offset + 4 + length (mismatched types int and uint32)
./puttygen.go:345: cannot assign int64 to numlines (type int) in multiple assignment
./puttygen.go:351: undefined: parseBlob
./puttygen.go:377: cannot assign int64 to numlines (type int) in multiple assignment
./puttygen.go:377: too many errors
Tested for two instances with OS X version of puttygen.
Available command line args via flag:
-comment="..." to add a comment
-passin="..." to decrypt the pem from stdin
-passout="..." to encrypt the ppk output