Commit 9c87d6c3 authored by ale's avatar ale

Add package to manage user encryption keys

Encryption keys are stored themselves encrypted using a password (kdf+aes-siv construction). This package was moved over from git.autistici.org/id/keystore and completely refactored.
parent 3a206d00
package main
import (
"encoding/base64"
"flag"
"fmt"
"log"
"git.autistici.org/ai3/go-common/userenckey"
)
var (
doGenKeys = flag.Bool("gen-keys", false, "generate user encryption keys with the specified curve")
doDecrypt = flag.Bool("decrypt", false, "decrypt the private key given on the command line")
password = flag.String("password", "", "password")
)
func genKeys() ([]byte, []byte, error) {
pub, priv, err := userenckey.GenerateKey()
if err != nil {
return nil, nil, err
}
enc, err := userenckey.Encrypt(priv, []byte(*password))
if err != nil {
return nil, nil, err
}
return enc, pub, err
}
func main() {
log.SetFlags(0)
flag.Parse()
switch {
case *doGenKeys:
if *password == "" {
log.Fatal("must specify --password")
}
priv, pub, err := genKeys()
if err != nil {
log.Fatal(err)
}
fmt.Printf(
"public key:\n%s\nprivate key (encrypted,base64): %s\n",
pub,
base64.StdEncoding.EncodeToString(priv),
)
case *doDecrypt:
if *password == "" {
log.Fatal("must specify --password")
}
if flag.NArg() < 1 {
log.Fatal("not enough arguments")
}
var encKeys [][]byte
for _, arg := range flag.Args() {
encKey, err := base64.StdEncoding.DecodeString(arg)
if err != nil {
log.Fatalf("bad base64-encoded argument: %v", err)
}
encKeys = append(encKeys, encKey)
}
dec, err := userenckey.Decrypt(encKeys, []byte(*password))
if err != nil {
log.Fatal(err)
}
pem, err := dec.PEM()
if err != nil {
log.Fatalf("invalid private key: %v", err)
}
fmt.Printf("private key:\n%s\n", pem)
default:
log.Fatal("no actions specified")
}
}
package userenckey
import (
"bytes"
"crypto/rand"
"errors"
"io"
"github.com/lunixbochs/struc"
"github.com/miscreant/miscreant/go"
"golang.org/x/crypto/argon2"
)
// Current algorithm: Argon2 KDF + AES-SIV key encryption.
const algoArgon2AESSIV = 1
const aeadAlgo = "AES-SIV"
// Struct members have stupid names to reduce the size of the resulting gob!
type argon2Params struct {
Time uint32 `struc:"uint32,little"`
Memory uint32 `struc:"uint32,little"`
Threads uint8 `struc:"uint8"`
}
// Default Argon2 parameters are tuned for a high-traffic
// authentication service (<1ms per operation).
var defaultArgon2Params = argon2Params{
Time: 1,
Memory: 4 * 1024,
Threads: 4,
}
const (
keyLen = 64
saltLen = 32
)
func argon2KDF(params argon2Params, salt, pw []byte) []byte {
return argon2.Key(pw, salt, params.Time, params.Memory, params.Threads, keyLen)
}
// An encrypted container stores an opaque blob of binary data along
// with metadata about the encryption itself, to allow for a
// controlled amount of algorithm malleability accounting for future
// updates. The structure is binary-packed (as opposed to using higher
// level serializations such as encoding/gob) because we'd like to be
// able to read it from other languages if necessary.
type container struct { // nolint: maligned
Algo uint8 `struc:"uint8"`
Params argon2Params
SaltLen uint8 `struc:"uint8,sizeof=Salt"`
Salt []byte
DataLen uint16 `struc:"uint16,little,sizeof=Data"`
Data []byte
}
// Convert to an opaque encoded ("wire") representation.
func (c *container) Marshal() ([]byte, error) {
var buf bytes.Buffer
err := struc.Pack(&buf, c)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Parse a key object from the wire representation.
func unmarshalContainer(b []byte) (c container, err error) {
err = struc.Unpack(bytes.NewReader(b), &c)
return
}
func newContainer(data, pw []byte) (container, error) {
return encryptArgon2AESSIV(data, pw)
}
func (c container) decrypt(pw []byte) ([]byte, error) {
// Only one supported kdf/algo combination right now.
if c.Algo == algoArgon2AESSIV {
return c.decryptArgon2AESSIV(pw)
}
return nil, errors.New("unsupported algo")
}
func (c container) decryptArgon2AESSIV(pw []byte) ([]byte, error) {
// Run the KDF and create the AEAD cipher.
dk := argon2KDF(c.Params, c.Salt, pw)
cipher, err := miscreant.NewAEAD(aeadAlgo, dk, 0)
if err != nil {
return nil, err
}
// Decrypt the data and obtain the DER-encoded private key.
dec, err := cipher.Open(nil, nil, c.Data, nil)
return dec, err
}
func encryptArgon2AESSIV(data, pw []byte) (container, error) {
c := container{
Algo: algoArgon2AESSIV,
Params: defaultArgon2Params,
Salt: genRandomSalt(),
}
// Run the KDF and create the AEAD cipher.
dk := argon2KDF(c.Params, c.Salt, pw)
cipher, err := miscreant.NewAEAD(aeadAlgo, dk, 0)
if err != nil {
return container{}, err
}
// Encrypt the data (a DER-encoded ECDSA private key).
c.Data = cipher.Seal(nil, nil, data, nil)
return c, nil
}
func genRandomSalt() []byte {
var b [saltLen]byte
if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
panic(err)
}
return b[:]
}
package userenckey
import (
"bytes"
"testing"
)
func TestArgon2AESSIV(t *testing.T) {
pw := []byte("secret pw")
encKey := []byte("secret encryption key")
key, err := encryptArgon2AESSIV(encKey, pw)
if err != nil {
t.Fatal("encryptArgon2AESSIV", err)
}
out, err := key.decryptArgon2AESSIV(pw)
if err != nil {
t.Fatal("decryptArgon2AESSIV", err)
}
if !bytes.Equal(out, encKey) {
t.Fatal("decryption failed")
}
}
package userenckey
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
)
// ErrBadPassword is returned on decryption failure.
var ErrBadPassword = errors.New("could not decrypt key with password")
func encodePublicKeyToPEM(pub *ecdsa.PublicKey) ([]byte, error) {
der, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der}), nil
}
// Key (unencrypted).
type Key struct {
rawBytes []byte
}
// GenerateKey generates a new ECDSA key pair, and returns the
// PEM-encoded public and private key (in order).
func GenerateKey() ([]byte, *Key, error) {
priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
if err != nil {
return nil, nil, err
}
//privBytes, err := encodePrivateKeyToPEM(priv)
privBytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return nil, nil, err
}
pubBytes, err := encodePublicKeyToPEM(&priv.PublicKey)
if err != nil {
return nil, nil, err
}
return pubBytes, &Key{privBytes}, nil
}
// PEM returns the key in PEM-encoded format.
func (k *Key) PEM() ([]byte, error) {
// Parse the ASN.1 data and encode it with PKCS8 (in PEM format).
priv, err := k.PrivateKey()
if err != nil {
return nil, err
}
return encodePrivateKeyToPEM(priv)
}
// PrivateKey parses the DER-encoded ASN.1 data in Key and returns the
// private key object.
func (k *Key) PrivateKey() (*ecdsa.PrivateKey, error) {
return x509.ParseECPrivateKey(k.rawBytes)
}
// Encrypt a key with a password and a random salt.
func Encrypt(key *Key, pw []byte) ([]byte, error) {
c, err := newContainer(key.rawBytes, pw)
if err != nil {
return nil, err
}
return c.Marshal()
}
// Decrypt one out of multiple keys with the specified password. The
// keys share the same cleartext, but have been encrypted with
// different passwords.
func Decrypt(encKeys [][]byte, pw []byte) (*Key, error) {
for _, encKey := range encKeys {
c, err := unmarshalContainer(encKey)
if err != nil {
//log.Printf("parse error: %v", err)
continue
}
if dec, err := c.decrypt(pw); err == nil {
return &Key{dec}, nil
}
}
return nil, ErrBadPassword
}
package userenckey
import (
"bytes"
"log"
"testing"
)
func TestGenerateKey(t *testing.T) {
pub, priv, err := GenerateKey()
if err != nil {
t.Fatal(err)
}
if !bytes.HasPrefix(pub, []byte("-----BEGIN PUBLIC KEY-----")) {
t.Errorf("bad public key: %s", string(pub))
}
if priv == nil {
t.Fatalf("no private key returned")
}
if len(priv.rawBytes) == 0 {
t.Fatalf("private key is empty")
}
// Parse the key now, check PKCS8 PEM header.
pem, err := priv.PEM()
if err != nil {
t.Fatalf("error parsing private key: %v", err)
}
if !bytes.HasPrefix(pem, []byte("-----BEGIN PRIVATE KEY-----")) {
t.Fatalf("bad PEM private key: %s", string(pem))
}
}
func TestEncryptDecrypt(t *testing.T) {
pw := []byte("stracchino")
// Don't need to use a real key as Encrypt/Decrypt are
// agnostic with respect to the container content.
key := &Key{[]byte("this is a very secret key")}
enc, err := Encrypt(key, pw)
if err != nil {
t.Fatal("Encrypt():", err)
}
log.Printf("encrypted key: %q (%d bytes)", enc, len(enc))
dec, err := Decrypt([][]byte{enc}, pw)
if err != nil {
t.Fatal("Decrypt():", err)
}
if !bytes.Equal(key.rawBytes, dec.rawBytes) {
t.Fatalf("bad decrypted ciphertext: %v", dec)
}
}
// +build go1.10
package userenckey
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
)
// Encode a private key to PEM-encoded PKCS8.
func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
der, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
return pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), nil
}
// +build !go1.10
package userenckey
import (
"bytes"
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"os/exec"
)
// Encode a private key to PEM-encoded PKCS8.
//
// In Go versions prior to 1.10, we must shell out to openssl to
// convert the private key to PKCS8 format.
func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
der, err := x509.MarshalECPrivateKey(priv)
if err != nil {
return nil, err
}
pkcs1 := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})
/* #nosec */
cmd := exec.Command("/usr/bin/openssl", "pkey")
cmd.Stdin = bytes.NewReader(pkcs1)
return cmd.Output()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment