123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- package cert
- import (
- "crypto/aes"
- "crypto/cipher"
- "crypto/ed25519"
- "crypto/rand"
- "encoding/pem"
- "fmt"
- "io"
- "math"
- "golang.org/x/crypto/argon2"
- "google.golang.org/protobuf/proto"
- )
- type NebulaEncryptedData struct {
- EncryptionMetadata NebulaEncryptionMetadata
- Ciphertext []byte
- }
- type NebulaEncryptionMetadata struct {
- EncryptionAlgorithm string
- Argon2Parameters Argon2Parameters
- }
- // Argon2Parameters KDF factors
- type Argon2Parameters struct {
- version rune
- Memory uint32 // KiB
- Parallelism uint8
- Iterations uint32
- salt []byte
- }
- // NewArgon2Parameters Returns a new Argon2Parameters object with current version set
- func NewArgon2Parameters(memory uint32, parallelism uint8, iterations uint32) *Argon2Parameters {
- return &Argon2Parameters{
- version: argon2.Version,
- Memory: memory, // KiB
- Parallelism: parallelism,
- Iterations: iterations,
- }
- }
- // Encrypts data using AES-256-GCM and the Argon2id key derivation function
- func aes256Encrypt(passphrase []byte, kdfParams *Argon2Parameters, data []byte) ([]byte, error) {
- key, err := aes256DeriveKey(passphrase, kdfParams)
- if err != nil {
- return nil, err
- }
- // this should never happen, but since this dictates how our calls into the
- // aes package behave and could be catastraphic, let's sanity check this
- if len(key) != 32 {
- return nil, fmt.Errorf("invalid AES-256 key length (%d) - cowardly refusing to encrypt", len(key))
- }
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- return nil, err
- }
- nonce := make([]byte, gcm.NonceSize())
- if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
- return nil, err
- }
- ciphertext := gcm.Seal(nil, nonce, data, nil)
- blob := joinNonceCiphertext(nonce, ciphertext)
- return blob, nil
- }
- // Decrypts data using AES-256-GCM and the Argon2id key derivation function
- // Expects the data to include an Argon2id parameter string before the encrypted data
- func aes256Decrypt(passphrase []byte, kdfParams *Argon2Parameters, data []byte) ([]byte, error) {
- key, err := aes256DeriveKey(passphrase, kdfParams)
- if err != nil {
- return nil, err
- }
- block, err := aes.NewCipher(key)
- if err != nil {
- return nil, err
- }
- gcm, err := cipher.NewGCM(block)
- if err != nil {
- return nil, err
- }
- nonce, ciphertext, err := splitNonceCiphertext(data, gcm.NonceSize())
- if err != nil {
- return nil, err
- }
- plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
- if err != nil {
- return nil, fmt.Errorf("invalid passphrase or corrupt private key")
- }
- return plaintext, nil
- }
- func aes256DeriveKey(passphrase []byte, params *Argon2Parameters) ([]byte, error) {
- if params.salt == nil {
- params.salt = make([]byte, 32)
- if _, err := rand.Read(params.salt); err != nil {
- return nil, err
- }
- }
- // keySize of 32 bytes will result in AES-256 encryption
- key, err := deriveKey(passphrase, 32, params)
- if err != nil {
- return nil, err
- }
- return key, nil
- }
- // Derives a key from a passphrase using Argon2id
- func deriveKey(passphrase []byte, keySize uint32, params *Argon2Parameters) ([]byte, error) {
- if params.version != argon2.Version {
- return nil, fmt.Errorf("incompatible Argon2 version: %d", params.version)
- }
- if params.salt == nil {
- return nil, fmt.Errorf("salt must be set in argon2Parameters")
- } else if len(params.salt) < 16 {
- return nil, fmt.Errorf("salt must be at least 128 bits")
- }
- key := argon2.IDKey(passphrase, params.salt, params.Iterations, params.Memory, params.Parallelism, keySize)
- return key, nil
- }
- // Prepends nonce to ciphertext
- func joinNonceCiphertext(nonce []byte, ciphertext []byte) []byte {
- return append(nonce, ciphertext...)
- }
- // Splits nonce from ciphertext
- func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
- if len(blob) <= nonceSize {
- return nil, nil, fmt.Errorf("invalid ciphertext blob - blob shorter than nonce length")
- }
- return blob[:nonceSize], blob[nonceSize:], nil
- }
- // EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
- func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
- ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
- if err != nil {
- return nil, err
- }
- b, err = proto.Marshal(&RawNebulaEncryptedData{
- EncryptionMetadata: &RawNebulaEncryptionMetadata{
- EncryptionAlgorithm: "AES-256-GCM",
- Argon2Parameters: &RawNebulaArgon2Parameters{
- Version: kdfParams.version,
- Memory: kdfParams.Memory,
- Parallelism: uint32(kdfParams.Parallelism),
- Iterations: kdfParams.Iterations,
- Salt: kdfParams.salt,
- },
- },
- Ciphertext: ciphertext,
- })
- if err != nil {
- return nil, err
- }
- switch curve {
- case Curve_CURVE25519:
- return pem.EncodeToMemory(&pem.Block{Type: EncryptedEd25519PrivateKeyBanner, Bytes: b}), nil
- case Curve_P256:
- return pem.EncodeToMemory(&pem.Block{Type: EncryptedECDSAP256PrivateKeyBanner, Bytes: b}), nil
- default:
- return nil, fmt.Errorf("invalid curve: %v", curve)
- }
- }
- // UnmarshalNebulaEncryptedData will unmarshal a protobuf byte representation of a nebula cert into its
- // protobuf-generated struct.
- func UnmarshalNebulaEncryptedData(b []byte) (*NebulaEncryptedData, error) {
- if len(b) == 0 {
- return nil, fmt.Errorf("nil byte array")
- }
- var rned RawNebulaEncryptedData
- err := proto.Unmarshal(b, &rned)
- if err != nil {
- return nil, err
- }
- if rned.EncryptionMetadata == nil {
- return nil, fmt.Errorf("encoded EncryptionMetadata was nil")
- }
- if rned.EncryptionMetadata.Argon2Parameters == nil {
- return nil, fmt.Errorf("encoded Argon2Parameters was nil")
- }
- params, err := unmarshalArgon2Parameters(rned.EncryptionMetadata.Argon2Parameters)
- if err != nil {
- return nil, err
- }
- ned := NebulaEncryptedData{
- EncryptionMetadata: NebulaEncryptionMetadata{
- EncryptionAlgorithm: rned.EncryptionMetadata.EncryptionAlgorithm,
- Argon2Parameters: *params,
- },
- Ciphertext: rned.Ciphertext,
- }
- return &ned, nil
- }
- func unmarshalArgon2Parameters(params *RawNebulaArgon2Parameters) (*Argon2Parameters, error) {
- if params.Version < math.MinInt32 || params.Version > math.MaxInt32 {
- return nil, fmt.Errorf("Argon2Parameters Version must be at least %d and no more than %d", math.MinInt32, math.MaxInt32)
- }
- if params.Memory <= 0 || params.Memory > math.MaxUint32 {
- return nil, fmt.Errorf("Argon2Parameters Memory must be be greater than 0 and no more than %d KiB", uint32(math.MaxUint32))
- }
- if params.Parallelism <= 0 || params.Parallelism > math.MaxUint8 {
- return nil, fmt.Errorf("Argon2Parameters Parallelism must be be greater than 0 and no more than %d", math.MaxUint8)
- }
- if params.Iterations <= 0 || params.Iterations > math.MaxUint32 {
- return nil, fmt.Errorf("-argon-iterations must be be greater than 0 and no more than %d", uint32(math.MaxUint32))
- }
- return &Argon2Parameters{
- version: params.Version,
- Memory: params.Memory,
- Parallelism: uint8(params.Parallelism),
- Iterations: params.Iterations,
- salt: params.Salt,
- }, nil
- }
- // DecryptAndUnmarshalSigningPrivateKey will try to pem decode and decrypt an Ed25519/ECDSA private key with
- // the given passphrase, returning any other bytes b or an error on failure
- func DecryptAndUnmarshalSigningPrivateKey(passphrase, b []byte) (Curve, []byte, []byte, error) {
- var curve Curve
- k, r := pem.Decode(b)
- if k == nil {
- return curve, nil, r, fmt.Errorf("input did not contain a valid PEM encoded block")
- }
- switch k.Type {
- case EncryptedEd25519PrivateKeyBanner:
- curve = Curve_CURVE25519
- case EncryptedECDSAP256PrivateKeyBanner:
- curve = Curve_P256
- default:
- return curve, nil, r, fmt.Errorf("bytes did not contain a proper nebula encrypted Ed25519/ECDSA private key banner")
- }
- ned, err := UnmarshalNebulaEncryptedData(k.Bytes)
- if err != nil {
- return curve, nil, r, err
- }
- var bytes []byte
- switch ned.EncryptionMetadata.EncryptionAlgorithm {
- case "AES-256-GCM":
- bytes, err = aes256Decrypt(passphrase, &ned.EncryptionMetadata.Argon2Parameters, ned.Ciphertext)
- if err != nil {
- return curve, nil, r, err
- }
- default:
- return curve, nil, r, fmt.Errorf("unsupported encryption algorithm: %s", ned.EncryptionMetadata.EncryptionAlgorithm)
- }
- switch curve {
- case Curve_CURVE25519:
- if len(bytes) != ed25519.PrivateKeySize {
- return curve, nil, r, fmt.Errorf("key was not %d bytes, is invalid ed25519 private key", ed25519.PrivateKeySize)
- }
- case Curve_P256:
- if len(bytes) != 32 {
- return curve, nil, r, fmt.Errorf("key was not 32 bytes, is invalid ECDSA P256 private key")
- }
- }
- return curve, bytes, r, nil
- }
|