package sshkeys

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/dsa"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"fmt"
	"math"
	"math/big"

	"github.com/dchest/bcrypt_pbkdf"
	"golang.org/x/crypto/ed25519"
	"golang.org/x/crypto/ssh"
)

const keySizeAES256 = 32

// Format of private key to use when Marshaling.
type Format int

const (
	// FormatOpenSSHv1 encodes a private key using OpenSSH's PROTOCOL.key format: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
	FormatOpenSSHv1 Format = iota
	// FormatClassicPEM encodes private keys in PEM, with a key-specific encoding, as used by OpenSSH.
	FormatClassicPEM
)

// MarshalOptions provides the Marshal function format and encryption options.
type MarshalOptions struct {
	// Passphrase to encrypt private key with, if nil, the key will not be encrypted.
	Passphrase []byte
	// Format to encode the private key in.
	Format Format
}

// Marshal converts a private key into an optionally encrypted format.
func Marshal(pk interface{}, opts *MarshalOptions) ([]byte, error) {
	switch opts.Format {
	case FormatOpenSSHv1:
		return marshalOpenssh(pk, opts)
	case FormatClassicPEM:
		return marshalPem(pk, opts)
	default:
		return nil, fmt.Errorf("sshkeys: invalid format %d", opts.Format)
	}
}

func marshalPem(pk interface{}, opts *MarshalOptions) ([]byte, error) {
	var err error
	var plain []byte
	var pemType string

	switch key := pk.(type) {
	case *rsa.PrivateKey:
		pemType = "RSA PRIVATE KEY"
		plain = x509.MarshalPKCS1PrivateKey(key)
	case *ecdsa.PrivateKey:
		pemType = "EC PRIVATE KEY"
		plain, err = x509.MarshalECPrivateKey(key)
		if err != nil {
			return nil, err
		}
	case *dsa.PrivateKey:
		pemType = "DSA PRIVATE KEY"
		plain, err = marshalDSAPrivateKey(key)
		if err != nil {
			return nil, err
		}
	case *ed25519.PrivateKey:
		return nil, fmt.Errorf("sshkeys: ed25519 keys must be marshaled with FormatOpenSSHv1")
	default:
		return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
	}

	if len(opts.Passphrase) > 0 {
		block, err := x509.EncryptPEMBlock(rand.Reader, pemType, plain, opts.Passphrase, x509.PEMCipherAES128)
		if err != nil {
			return nil, err
		}
		return pem.EncodeToMemory(block), nil
	}

	return pem.EncodeToMemory(&pem.Block{
		Type:  pemType,
		Bytes: plain,
	}), nil
}

type dsaOpenssl struct {
	Version int
	P       *big.Int
	Q       *big.Int
	G       *big.Int
	Pub     *big.Int
	Priv    *big.Int
}

// https://github.com/golang/crypto/blob/master/ssh/keys.go#L793-L804
func marshalDSAPrivateKey(pk *dsa.PrivateKey) ([]byte, error) {
	k := dsaOpenssl{
		Version: 0,
		P:       pk.P,
		Q:       pk.Q,
		G:       pk.G,
		Pub:     pk.Y,
		Priv:    pk.X,
	}

	return asn1.Marshal(k)
}

const opensshv1Magic = "openssh-key-v1"

type opensshHeader struct {
	CipherName   string
	KdfName      string
	KdfOpts      string
	NumKeys      uint32
	PubKey       string
	PrivKeyBlock string
}

type opensshKey struct {
	Check1  uint32
	Check2  uint32
	Keytype string
	Rest    []byte `ssh:"rest"`
}

type opensshRsa struct {
	N       *big.Int
	E       *big.Int
	D       *big.Int
	Iqmp    *big.Int
	P       *big.Int
	Q       *big.Int
	Comment string
	Pad     []byte `ssh:"rest"`
}

type opensshED25519 struct {
	Pub     []byte
	Priv    []byte
	Comment string
	Pad     []byte `ssh:"rest"`
}

func padBytes(data []byte, blocksize int) []byte {
	if blocksize != 0 {
		var i byte
		for i = byte(1); len(data)%blocksize != 0; i++ {
			data = append(data, i&0xFF)
		}
	}
	return data
}

func marshalOpenssh(pk interface{}, opts *MarshalOptions) ([]byte, error) {
	var blocksize int
	var keylen int

	out := opensshHeader{
		CipherName: "none",
		KdfName:    "none",
		KdfOpts:    "",
		NumKeys:    1,
		PubKey:     "",
	}

	if len(opts.Passphrase) > 0 {
		out.CipherName = "aes256-cbc"
		out.KdfName = "bcrypt"
		keylen = keySizeAES256
		blocksize = aes.BlockSize
	}

	// Get a crypto rand in the range [0, uint32-max]
	randnum, err := rand.Int(rand.Reader, big.NewInt(math.MaxUint32+1))
	if err != nil {
		return nil, fmt.Errorf("sshkeys: failed to get random number: %w", err)
	}
	// Convert it to uint32. As the content was within uint32 limits, nothing should be lost
	check := uint32(randnum.Uint64())

	pk1 := opensshKey{
		Check1: check,
		Check2: check,
	}

	switch key := pk.(type) {
	case *rsa.PrivateKey:
		k := &opensshRsa{
			N:       key.N,
			E:       big.NewInt(int64(key.E)),
			D:       key.D,
			Iqmp:    key.Precomputed.Qinv,
			P:       key.Primes[0],
			Q:       key.Primes[1],
			Comment: "",
		}

		data := ssh.Marshal(k)
		pk1.Keytype = ssh.KeyAlgoRSA
		pk1.Rest = data
		publicKey, err := ssh.NewPublicKey(&key.PublicKey)
		if err != nil {
			return nil, err
		}
		out.PubKey = string(publicKey.Marshal())

	case ed25519.PrivateKey:
		k := opensshED25519{
			Pub:  key.Public().(ed25519.PublicKey),
			Priv: key,
		}
		data := ssh.Marshal(k)
		pk1.Keytype = ssh.KeyAlgoED25519
		pk1.Rest = data

		publicKey, err := ssh.NewPublicKey(key.Public())
		if err != nil {
			return nil, err
		}
		out.PubKey = string(publicKey.Marshal())
	case *ed25519.PrivateKey:
		k := opensshED25519{
			Pub:  key.Public().(ed25519.PublicKey),
			Priv: *key,
		}
		data := ssh.Marshal(k)
		pk1.Keytype = ssh.KeyAlgoED25519
		pk1.Rest = data

		publicKey, err := ssh.NewPublicKey(key.Public())
		if err != nil {
			return nil, err
		}
		out.PubKey = string(publicKey.Marshal())
	default:
		return nil, fmt.Errorf("sshkeys: unsupported key type %T", pk)
	}

	if len(opts.Passphrase) > 0 {
		rounds := 16
		ivlen := blocksize
		salt := make([]byte, blocksize)
		_, err := rand.Read(salt)
		if err != nil {
			return nil, err
		}

		kdfdata, err := bcrypt_pbkdf.Key(opts.Passphrase, salt, rounds, keylen+ivlen)
		if err != nil {
			return nil, err
		}
		iv := kdfdata[keylen : ivlen+keylen]
		aeskey := kdfdata[0:keylen]

		block, err := aes.NewCipher(aeskey)
		if err != nil {
			return nil, err
		}

		pkblock := padBytes(ssh.Marshal(pk1), blocksize)

		cbc := cipher.NewCBCEncrypter(block, iv)
		cbc.CryptBlocks(pkblock, pkblock)

		out.PrivKeyBlock = string(pkblock)

		var opts struct {
			Salt   []byte
			Rounds uint32
		}

		opts.Salt = salt
		opts.Rounds = uint32(rounds)

		out.KdfOpts = string(ssh.Marshal(&opts))
	} else {
		out.PrivKeyBlock = string(ssh.Marshal(pk1))
	}

	outBytes := []byte(opensshv1Magic)
	outBytes = append(outBytes, 0)
	outBytes = append(outBytes, ssh.Marshal(out)...)
	block := &pem.Block{
		Type:  "OPENSSH PRIVATE KEY",
		Bytes: outBytes,
	}
	return pem.EncodeToMemory(block), nil
}
