2019-11-02 12:44:04 -05:00

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
}