Commit 5c61e9fc authored by ale's avatar ale

Use the userenckey package from ai3/go-common

parent 30459825
Pipeline #1019 passed with stages
in 56 seconds
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")
}
}
......@@ -8,10 +8,10 @@ import (
"strings"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/ai3/go-common/userenckey"
"git.autistici.org/id/keystore/backend"
"git.autistici.org/id/keystore/client"
"git.autistici.org/id/keystore/userenckey"
)
// Config for the dovecot-keystore daemon.
......@@ -139,11 +139,16 @@ func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password st
log.Printf("failed passdb lookup for %s (no keys)", username)
return nil, false, nil
}
priv, err = userenckey.Decrypt(encKeys, []byte(password))
key, err := userenckey.Decrypt(encKeys, []byte(password))
if err != nil {
log.Printf("failed passdb lookup for %s (could not decrypt key)", username)
return nil, false, err
}
priv, err = key.PEM()
if err != nil {
log.Printf("failed passdb lookup for %s (obtained invalid key: %v)", username, err)
return nil, false, err
}
log.Printf("passdb lookup for %s (decrypted)", username)
return &passdbResponse{PrivateKey: s.b64encode(priv)}, true, nil
}
......
......@@ -9,10 +9,10 @@ import (
"sync"
"time"
"git.autistici.org/ai3/go-common/userenckey"
"git.autistici.org/id/go-sso"
"git.autistici.org/id/keystore/backend"
"git.autistici.org/id/keystore/userenckey"
)
var (
......@@ -159,10 +159,16 @@ func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSecon
if err != nil {
return err
}
// Obtain the PEM representation of the private key and store
// that in memory.
pem, err := pkey.PEM()
if err != nil {
return err
}
s.mx.Lock()
s.userKeys[username] = userKey{
pkey: pkey,
pkey: pem,
expiry: time.Now().Add(time.Duration(ttlSeconds) * time.Second),
}
s.mx.Unlock()
......
......@@ -9,10 +9,10 @@ import (
"testing"
"time"
"git.autistici.org/ai3/go-common/userenckey"
"golang.org/x/crypto/ed25519"
"git.autistici.org/id/go-sso"
"git.autistici.org/id/keystore/userenckey"
)
type testContext struct {
......@@ -68,13 +68,14 @@ func (t *testDB) GetPrivateKeys(_ context.Context, username string) ([][]byte, e
}
var (
privKey = []byte("fairly secret key")
privKey *userenckey.Key
pw = []byte("equally secret password")
encPrivKey []byte
)
func init() {
var err error
_, privKey, _ = userenckey.GenerateKey()
encPrivKey, err = userenckey.Encrypt(privKey, pw)
if err != nil {
panic(err)
......@@ -115,8 +116,10 @@ func TestKeystore_OpenAndGet(t *testing.T) {
if err != nil {
t.Fatal("keystore.Get():", err)
}
if !bytes.Equal(result, privKey) {
t.Fatalf("keystore.Get() returned bad key: got %v, expected %v", result, privKey)
expectedPEM, _ := privKey.PEM()
if !bytes.Equal(result, expectedPEM) {
t.Fatalf("keystore.Get() returned bad key: got %v, expected %v", result, expectedPEM)
}
}
......
package userenckey
import (
"errors"
"github.com/miscreant/miscreant/go"
"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
scryptP = 1
keyLen = 64
saltLen = 32
)
// 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) ([]byte, error) {
for _, key := range encKeys {
dec, err := decryptData(key, pw)
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 {
return nil, errors.New("short data")
}
salt := data[:saltLen]
data = data[saltLen:]
// Apply the key derivation function to the password to obtain
// a 64 byte key.
dk, err := kdf(pw, salt)
if err != nil {
return nil, err
}
// Set up the AES-SIV secret box.
cipher, err := miscreant.NewAEAD(aeadAlgo, dk, 0)
if err != nil {
return nil, err
}
return cipher.Open(nil, nil, data, nil)
}
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[:]
}
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)
}
}
package userenckey
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
)
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
}
// GenerateKey generates a new ECDSA key pair, and returns the
// PEM-encoded public and private key (in order).
func GenerateKey() ([]byte, []byte, error) {
pkey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
if err != nil {
return nil, nil, err
}
privBytes, err := encodePrivateKeyToPEM(pkey)
if err != nil {
return nil, nil, err
}
pubBytes, err := encodePublicKeyToPEM(&pkey.PublicKey)
if err != nil {
return nil, nil, err
}
return pubBytes, privBytes, nil
}
package userenckey
import (
"bytes"
"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 !bytes.HasPrefix(priv, []byte("-----BEGIN PRIVATE KEY-----")) {
t.Errorf("bad private key: %s", string(priv))
}
}
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 (
"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
}
......@@ -21,6 +21,7 @@ func encodePrivateKeyToPEM(priv *ecdsa.PrivateKey) ([]byte, error) {
}
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()
......
Copyright (c) 2015 Ryan Hileman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
[![Build Status](https://travis-ci.org/lunixbochs/struc.svg?branch=master)](https://travis-ci.org/lunixbochs/struc)
struc
====
Struc exists to pack and unpack C-style structures from bytes, which is useful for binary files and network protocols. It could be considered an alternative to `encoding/binary`, which requires massive boilerplate for some similar operations.
Take a look at an [example comparing `struc` and `encoding/binary`](https://bochs.info/p/cxvm9)
Struc considers usability first. That said, it does cache reflection data and aims to be competitive with `encoding/binary` struct packing in every way, including performance.
Example struct
----
```Go
type Example struct {
Var int `struc:"int32,sizeof=Str"`
Str string
Weird []byte `struc:"[8]int64"`
Var []int `struc:"[]int32,little"`
}
```
Struct tag format
----
- ```Var []int `struc:"[]int32,little,sizeof=StringField"` ``` will pack Var as a slice of little-endian int32, and link it as the size of `StringField`.
- `sizeof=`: Indicates this field is a number used to track the length of a another field. `sizeof` fields are automatically updated on `Pack()` based on the current length of the tracked field, and are used to size the target field during `Unpack()`.
- Bare values will be parsed as type and endianness.
Endian formats
----
- `big` (default)
- `little`
Recognized types
----
- `pad` - this type ignores field contents and is backed by a `[length]byte` containing nulls
- `bool`
- `byte`
- `int8`, `uint8`
- `int16`, `uint16`
- `int32`, `uint32`
- `int64`, `uint64`
- `float32`
- `float64`
Types can be indicated as arrays/slices using `[]` syntax. Example: `[]int64`, `[8]int32`.
Bare slice types (those with no `[size]`) must have a linked `Sizeof` field.
Private fields are ignored when packing and unpacking.
Example code
----
```Go
package main
import (
"bytes"
"github.com/lunixbochs/struc"
)
type Example struct {
A int `struc:"big"`
// B will be encoded/decoded as a 16-bit int (a "short")
// but is stored as a native int in the struct
B int `struc:"int16"`
// the sizeof key links a buffer's size to any int field
Size int `struc:"int8,little,sizeof=Str"`
Str string
// you can get freaky if you want
Str2 string `struc:"[5]int64"`
}
func main() {
var buf bytes.Buffer
t := &Example{1, 2, 0, "test", "test2"}
err := struc.Pack(&buf, t)
o := &Example{}
err = struc.Unpack(&buf, o)
}
```