-
-
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