245 lines
6.1 KiB
Go
245 lines
6.1 KiB
Go
// Portions of this file are based on https://github.com/golang/crypto/blob/master/ssh/keys.go
|
|
//
|
|
// Copyright 2012 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package sshkeys
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"github.com/dchest/bcrypt_pbkdf"
|
|
"golang.org/x/crypto/ed25519"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// ErrIncorrectPassword is returned when the supplied passphrase was not correct for an encrypted private key.
|
|
var ErrIncorrectPassword = errors.New("sshkeys: Invalid Passphrase")
|
|
|
|
const keySizeAES256 = 32
|
|
|
|
// ParseEncryptedPrivateKey returns a Signer from an encrypted private key. It supports
|
|
// the same keys as ParseEncryptedRawPrivateKey.
|
|
func ParseEncryptedPrivateKey(data []byte, passphrase []byte) (ssh.Signer, error) {
|
|
key, err := ParseEncryptedRawPrivateKey(data, passphrase)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ssh.NewSignerFromKey(key)
|
|
}
|
|
|
|
// ParseEncryptedRawPrivateKey returns a private key from an encrypted private key. It
|
|
// supports RSA (PKCS#1 or OpenSSH), DSA (OpenSSL), and ECDSA private keys.
|
|
//
|
|
// ErrIncorrectPassword will be returned if the supplied passphrase is wrong,
|
|
// but some formats like RSA in PKCS#1 detecting a wrong passphrase is difficult,
|
|
// and other parse errors may be returned.
|
|
func ParseEncryptedRawPrivateKey(data []byte, passphrase []byte) (interface{}, error) {
|
|
var err error
|
|
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
return nil, errors.New("no PEM block found")
|
|
}
|
|
|
|
if x509.IsEncryptedPEMBlock(block) {
|
|
data, err = x509.DecryptPEMBlock(block, passphrase)
|
|
if err == x509.IncorrectPasswordError {
|
|
return nil, ErrIncorrectPassword
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
data = block.Bytes
|
|
}
|
|
|
|
switch block.Type {
|
|
case "RSA PRIVATE KEY":
|
|
pk, err := x509.ParsePKCS1PrivateKey(data)
|
|
if err != nil {
|
|
// The Algos for PEM Encryption do not include strong message authentication,
|
|
// so sometimes DecryptPEMBlock works, but ParsePKCS1PrivateKey fails with an asn1 error.
|
|
// We are just catching the most common prefix here...
|
|
if strings.HasPrefix(err.Error(), "asn1: structure error") {
|
|
return nil, ErrIncorrectPassword
|
|
}
|
|
return nil, err
|
|
}
|
|
return pk, nil
|
|
case "EC PRIVATE KEY":
|
|
return x509.ParseECPrivateKey(data)
|
|
case "DSA PRIVATE KEY":
|
|
return ssh.ParseDSAPrivateKey(data)
|
|
case "OPENSSH PRIVATE KEY":
|
|
return parseOpenSSHPrivateKey(data, passphrase)
|
|
default:
|
|
return nil, fmt.Errorf("sshkeys: unsupported key type %q", block.Type)
|
|
}
|
|
}
|
|
|
|
func parseOpenSSHPrivateKey(data []byte, passphrase []byte) (interface{}, error) {
|
|
magic := append([]byte(opensshv1Magic), 0)
|
|
if !bytes.Equal(magic, data[0:len(magic)]) {
|
|
return nil, errors.New("sshkeys: invalid openssh private key format")
|
|
}
|
|
remaining := data[len(magic):]
|
|
|
|
w := opensshHeader{}
|
|
|
|
if err := ssh.Unmarshal(remaining, &w); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if w.NumKeys != 1 {
|
|
return nil, fmt.Errorf("sshkeys: NumKeys must be 1: %d", w.NumKeys)
|
|
}
|
|
|
|
var privateKeyBytes []byte
|
|
var encrypted bool
|
|
|
|
switch {
|
|
// OpenSSH supports bcrypt KDF w/ AES256-CBC or AES256-CTR mode
|
|
case w.KdfName == "bcrypt" && w.CipherName == "aes256-cbc":
|
|
iv, block, err := extractBcryptIvBlock(passphrase, w)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cbc := cipher.NewCBCDecrypter(block, iv)
|
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
|
cbc.CryptBlocks(privateKeyBytes, privateKeyBytes)
|
|
|
|
encrypted = true
|
|
|
|
case w.KdfName == "bcrypt" && w.CipherName == "aes256-ctr":
|
|
iv, block, err := extractBcryptIvBlock(passphrase, w)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stream := cipher.NewCTR(block, iv)
|
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
|
stream.XORKeyStream(privateKeyBytes, privateKeyBytes)
|
|
|
|
encrypted = true
|
|
|
|
case w.KdfName == "none" && w.CipherName == "none":
|
|
privateKeyBytes = []byte(w.PrivKeyBlock)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("sshkeys: unknown Cipher/KDF: %s:%s", w.CipherName, w.KdfName)
|
|
}
|
|
|
|
pk1 := opensshKey{}
|
|
|
|
if err := ssh.Unmarshal(privateKeyBytes, &pk1); err != nil {
|
|
if encrypted {
|
|
return nil, ErrIncorrectPassword
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if pk1.Check1 != pk1.Check2 {
|
|
return nil, ErrIncorrectPassword
|
|
}
|
|
|
|
// we only handle ed25519 and rsa keys currently
|
|
switch pk1.Keytype {
|
|
case ssh.KeyAlgoRSA:
|
|
// https://github.com/openssh/openssh-portable/blob/V_7_4_P1/sshkey.c#L2760-L2773
|
|
key := opensshRsa{}
|
|
|
|
err := ssh.Unmarshal(pk1.Rest, &key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, b := range key.Pad {
|
|
if int(b) != i+1 {
|
|
return nil, errors.New("sshkeys: padding not as expected")
|
|
}
|
|
}
|
|
|
|
pk := &rsa.PrivateKey{
|
|
PublicKey: rsa.PublicKey{
|
|
N: key.N,
|
|
E: int(key.E.Int64()),
|
|
},
|
|
D: key.D,
|
|
Primes: []*big.Int{key.P, key.Q},
|
|
}
|
|
|
|
err = pk.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pk.Precompute()
|
|
|
|
return pk, nil
|
|
case ssh.KeyAlgoED25519:
|
|
key := opensshED25519{}
|
|
|
|
err := ssh.Unmarshal(pk1.Rest, &key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(key.Priv) != ed25519.PrivateKeySize {
|
|
return nil, errors.New("sshkeys: private key unexpected length")
|
|
}
|
|
|
|
for i, b := range key.Pad {
|
|
if int(b) != i+1 {
|
|
return nil, errors.New("sshkeys: padding not as expected")
|
|
}
|
|
}
|
|
|
|
pk := ed25519.PrivateKey(make([]byte, ed25519.PrivateKeySize))
|
|
copy(pk, key.Priv)
|
|
return pk, nil
|
|
default:
|
|
return nil, errors.New("sshkeys: unhandled key type")
|
|
}
|
|
}
|
|
|
|
func extractBcryptIvBlock(passphrase []byte, w opensshHeader) ([]byte, cipher.Block, error) {
|
|
cipherKeylen := keySizeAES256
|
|
cipherIvLen := aes.BlockSize
|
|
|
|
var opts struct {
|
|
Salt []byte
|
|
Rounds uint32
|
|
}
|
|
|
|
if err := ssh.Unmarshal([]byte(w.KdfOpts), &opts); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
kdfdata, err := bcrypt_pbkdf.Key(passphrase, opts.Salt, int(opts.Rounds), cipherKeylen+cipherIvLen)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
iv := kdfdata[cipherKeylen : cipherIvLen+cipherKeylen]
|
|
aeskey := kdfdata[0:cipherKeylen]
|
|
block, err := aes.NewCipher(aeskey)
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return iv, block, nil
|
|
}
|