From 29268777b907394c3ef9088b7ed8971b682341a7 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sat, 13 Jan 2018 15:01:03 +0000
Subject: [PATCH] Add encryption for user keys, and a command-line tool for
 testing

---
 cmd/userenckey-tool/main.go | 64 +++++++++++++++++++++++++++++++++++++
 userenckey/decrypt.go       | 17 +++++++---
 userenckey/encrypt.go       | 32 +++++++++++++++++++
 userenckey/encrypt_test.go  | 24 ++++++++++++++
 4 files changed, 132 insertions(+), 5 deletions(-)
 create mode 100644 cmd/userenckey-tool/main.go
 create mode 100644 userenckey/encrypt.go
 create mode 100644 userenckey/encrypt_test.go

diff --git a/cmd/userenckey-tool/main.go b/cmd/userenckey-tool/main.go
new file mode 100644
index 00000000..13b021eb
--- /dev/null
+++ b/cmd/userenckey-tool/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+	"bytes"
+	"encoding/base64"
+	"flag"
+	"fmt"
+	"log"
+	"os/exec"
+
+	"git.autistici.org/id/keystore/userenckey"
+)
+
+var (
+	doGenKeys = flag.Bool("gen-keys", false, "generate user encryption keys with the specified curve")
+	curve     = flag.String("curve", "secp224r1", "EC curve to use")
+	password  = flag.String("password", "", "password")
+)
+
+func genKeys() ([]byte, []byte, error) {
+	priv, err := exec.Command("sh", "-c", fmt.Sprintf("openssl ecparam -name %s -genkey | openssl pkey", *curve)).Output()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cmd := exec.Command("sh", "-c", "openssl ec -pubout")
+	cmd.Stdin = bytes.NewReader(priv)
+	pub, err := cmd.Output()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	priv, err = userenckey.Encrypt(priv, []byte(*password))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return priv, 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: %s\nprivate key (encrypted): %s\n",
+			base64.StdEncoding.EncodeToString(pub),
+			base64.StdEncoding.EncodeToString(priv),
+		)
+
+	default:
+		log.Fatal("no actions specified")
+	}
+}
diff --git a/userenckey/decrypt.go b/userenckey/decrypt.go
index 3f59eca6..99d7f711 100644
--- a/userenckey/decrypt.go
+++ b/userenckey/decrypt.go
@@ -7,8 +7,11 @@ import (
 	"golang.org/x/crypto/scrypt"
 )
 
+// ErrBadPassword is returned on decryption failure.
 var ErrBadPassword = errors.New("could not decrypt key with password")
 
+const aeadAlgo = "AES-SIV"
+
 const (
 	scryptN = 32768
 	scryptR = 8
@@ -23,13 +26,17 @@ const (
 func Decrypt(encKeys [][]byte, pw []byte) ([]byte, error) {
 	for _, key := range encKeys {
 		dec, err := decryptData(key, pw)
-		if err != nil {
-			return dec, err
+		if err == nil {
+			return dec, nil
 		}
 	}
 	return nil, ErrBadPassword
 }
 
+func kdf(pw, salt []byte) ([]byte, error) {
+	return scrypt.Key(pw, salt, scryptN, scryptR, scryptP, keyLen)
+}
+
 func decryptData(data, pw []byte) ([]byte, error) {
 	// The KDF salt is prepended to the encrypted key.
 	if len(data) < saltLen {
@@ -40,16 +47,16 @@ func decryptData(data, pw []byte) ([]byte, error) {
 
 	// Apply the key derivation function to the password to obtain
 	// a 64 byte key.
-	dk, err := scrypt.Key(pw, salt, scryptN, scryptR, scryptP, keyLen)
+	dk, err := kdf(pw, salt)
 	if err != nil {
 		return nil, err
 	}
 
 	// Set up the AES-SIV secret box.
-	cipher, err := miscreant.NewAESCMACSIV(dk)
+	cipher, err := miscreant.NewAEAD(aeadAlgo, dk, 0)
 	if err != nil {
 		return nil, err
 	}
 
-	return cipher.Open(nil, data)
+	return cipher.Open(nil, nil, data, nil)
 }
diff --git a/userenckey/encrypt.go b/userenckey/encrypt.go
new file mode 100644
index 00000000..877b0381
--- /dev/null
+++ b/userenckey/encrypt.go
@@ -0,0 +1,32 @@
+package userenckey
+
+import (
+	"crypto/rand"
+	"io"
+
+	"github.com/miscreant/miscreant/go"
+)
+
+// Encrypt a key with a password and a random salt.
+func Encrypt(key, pw []byte) ([]byte, error) {
+	salt := genRandomSalt()
+	dk, err := kdf(pw, salt)
+	if err != nil {
+		return nil, err
+	}
+
+	cipher, err := miscreant.NewAEAD(aeadAlgo, dk, 0)
+	if err != nil {
+		return nil, err
+	}
+
+	return cipher.Seal(salt, nil, key, nil), nil
+}
+
+func genRandomSalt() []byte {
+	var b [saltLen]byte
+	if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
+		panic(err)
+	}
+	return b[:]
+}
diff --git a/userenckey/encrypt_test.go b/userenckey/encrypt_test.go
new file mode 100644
index 00000000..36220c26
--- /dev/null
+++ b/userenckey/encrypt_test.go
@@ -0,0 +1,24 @@
+package userenckey
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestEncrypt(t *testing.T) {
+	pw := []byte("stracchino")
+	key := []byte("this is a very secret key")
+
+	enc, err := Encrypt(key, pw)
+	if err != nil {
+		t.Fatal("Encrypt():", err)
+	}
+
+	dec, err := Decrypt([][]byte{enc}, pw)
+	if err != nil {
+		t.Fatal("Decrypt():", err)
+	}
+	if !bytes.Equal(key, dec) {
+		t.Fatalf("bad decrypted ciphertext: %v", dec)
+	}
+}
-- 
GitLab