THIS IS A BREAKING CHANGE! Private Keys generated by previous versions won't work with this version.
* Refactored the private key protection to use standard AEAD construction. * Fix sanity check of decrypted block length to stay within verified bounds * Cleanup test harness to split into utility file (assert()); cleaned up names of test functions. * Fixed scrypt params to not take too long (N=2^19) * Updated README with these changes
This commit is contained in:
parent
262a554356
commit
f32525a864
8 changed files with 231 additions and 235 deletions
52
README.md
52
README.md
|
@ -139,16 +139,8 @@ recipient can decrypt using their private key.
|
|||
## Technical Details
|
||||
|
||||
### How is the private key protected?
|
||||
The Ed25519 private key is encrypted using a key derived from the
|
||||
user supplied pass phrase. This pass phrase is used to derive an
|
||||
encryption key using the Scrypt key derivation algorithm. The
|
||||
resulting derived key is XOR'd with the Ed25519 private key before
|
||||
being committed to disk. To protect the integrity of the process,
|
||||
the essential parameters used for deriving the key, and the derived
|
||||
key are hashed via SHA256 and stored along with the encrypted key.
|
||||
|
||||
As an additional security measure, the user supplied pass phrase is
|
||||
hashed with SHA512.
|
||||
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key
|
||||
derived from the user's pass phrase.
|
||||
|
||||
### How is the Encryption done?
|
||||
The file encryption uses AES-GCM-256 in AEAD mode. The encryption uses
|
||||
|
@ -158,7 +150,7 @@ is 4MB (4 * 1048576 bytes). Each chunk generates its own nonce
|
|||
from a global salt. The nonce is calculated as a SHA256 hash of
|
||||
the salt, the chunk length and the block number.
|
||||
|
||||
### What is the public-key cryptography used?
|
||||
### What is the public-key cryptography?
|
||||
`sigtool` uses Curve25519 ECC to generate shared secrets between
|
||||
pairs of sender & recipients. This pairwise shared secret is expanded
|
||||
using HKDF to generate a key-encryption-key. The file-encryption key
|
||||
|
@ -170,14 +162,20 @@ corresponding Curve25519 points in order to generate the shared secret.
|
|||
This elliptic co-ordinate transform follows [FiloSottile's writeup][2].
|
||||
|
||||
### Format of the Encrypted File
|
||||
Every encrypted file starts with a header:
|
||||
Every encrypted file starts with a header and the header-checksum:
|
||||
|
||||
* Fixed-size header
|
||||
* Variable-length header
|
||||
* SHA256 sum of both of the above
|
||||
|
||||
The fixed length header is:
|
||||
|
||||
7 byte magic ("SigTool")
|
||||
1 byte version number
|
||||
4 byte header length (big endian encoding)
|
||||
32 byte SHA256 of the encryption-header
|
||||
|
||||
The encryption-header is described as a protobuf file (sign/hdr.proto):
|
||||
The variable length header has the per-recipient wrapped keys. This is
|
||||
described as a protobuf file (sign/hdr.proto):
|
||||
|
||||
```protobuf
|
||||
message header {
|
||||
|
@ -194,6 +192,8 @@ The encryption-header is described as a protobuf file (sign/hdr.proto):
|
|||
}
|
||||
```
|
||||
|
||||
The SHA256 sum covers the fixed-length and variable-length headers.
|
||||
|
||||
The encrypted data immediately follows the headers above. Each encrypted
|
||||
chunk is encoded the same way:
|
||||
|
||||
|
@ -239,36 +239,30 @@ And, a serialized Ed25519 private key looks like so:
|
|||
esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ==
|
||||
salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s=
|
||||
algo: scrypt-sha256
|
||||
verify: JvjRjJMKhJhBmZngC3Pvq7x3KCLKt7gar1AAz7HB4qM=
|
||||
Z: 131072
|
||||
r: 16
|
||||
p: 1
|
||||
```
|
||||
|
||||
The Ed25519 private key is encrypted using Scrypt password hashing
|
||||
mechanism. A user supplied passphrase to protect the private key
|
||||
is first pre-hashed using SHA-512 before being used in
|
||||
```scrypt()```. In pseudo code, this operation looks like below:
|
||||
The Ed25519 private key is encrypted using AES-256-GCM AEAD mode;
|
||||
the encryption key is derived from the user supplied passphrase
|
||||
using scrypt KDF. A user supplied passphrase is first expanded
|
||||
using SHA-512 before being used in ```scrypt()```. In pseudo code,
|
||||
this operation looks like below:
|
||||
|
||||
passphrase = get_user_passphrase()
|
||||
hpass = SHA512(passphrase)
|
||||
salt = randombytes(32)
|
||||
xorkey = Scrypt(hpass, salt, N, r, p)
|
||||
verify = SHA256(salt, xorkey)
|
||||
esk = ed25519_private_key ^ xorkey
|
||||
key = Scrypt(hpass, salt, N, r, p)
|
||||
esk = AES256_GCM(ed25519_private_key, key)
|
||||
|
||||
Where, ```N```, ```r```, ```p``` are Scrypt parameters. In our
|
||||
implementation:
|
||||
|
||||
N = 131072
|
||||
r = 16
|
||||
N = 2^19 (1 << 19)
|
||||
r = 8
|
||||
p = 1
|
||||
|
||||
```verify``` is used during the decryption of the Ed25519 private
|
||||
key - *before* actually doing the "xor" operation. This check
|
||||
ensures that the supplied passphrase yields the same value as
|
||||
```verify```.
|
||||
|
||||
### Ed25519 Signature
|
||||
A generated signature looks like below after serialization:
|
||||
|
||||
|
|
1
go.sum
1
go.sum
|
@ -22,6 +22,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
|
|
161
sign/encrypt.go
161
sign/encrypt.go
|
@ -13,15 +13,19 @@
|
|||
//
|
||||
// Notes
|
||||
// =====
|
||||
// Header: Fixed size + Variable list of recipients
|
||||
// Header: has 3 parts:
|
||||
// - Fixed sized header
|
||||
// - Variable sized protobuf encoded header
|
||||
// - SHA256 sum of both above.
|
||||
//
|
||||
// Fixed size header:
|
||||
// - Magic: 7 bytes
|
||||
// - Version: 1 byte
|
||||
// - VLen: 4 byte
|
||||
//
|
||||
// Variable Length Segment:
|
||||
// - Protobuf encoded, per-recipient wrapped keys
|
||||
// - Shasum: 32 bytes (SHA256 of full header)
|
||||
// - Protobuf encoded recipient info
|
||||
//
|
||||
// The variable length segment consists of one or more
|
||||
// recipients, their wrapped keys etc. This is encoded as
|
||||
|
@ -48,14 +52,16 @@ import (
|
|||
)
|
||||
|
||||
// Encryption chunk size = 4MB
|
||||
const chunkSize uint32 = 4 * 1048576
|
||||
const maxChunkSize uint32 = 16 * 1048576
|
||||
const EOF uint32 = 1 << 31
|
||||
const (
|
||||
chunkSize uint32 = 4 * 1048576
|
||||
maxChunkSize uint32 = 16 * 1048576
|
||||
_EOF uint32 = 1 << 31
|
||||
|
||||
const _Magic = "SigTool"
|
||||
const _MagicLen = len(_Magic)
|
||||
const _AEADNonceLen = 32
|
||||
const _FixedHdrLen = _MagicLen + 1 + 4
|
||||
_Magic = "SigTool"
|
||||
_MagicLen = len(_Magic)
|
||||
_AEADNonceLen = 32
|
||||
_FixedHdrLen = _MagicLen + 1 + 4
|
||||
)
|
||||
|
||||
// Encryptor holds the encryption context
|
||||
type Encryptor struct {
|
||||
|
@ -73,7 +79,6 @@ type Encryptor struct {
|
|||
// signing any recipient keys. If 'sk' is nil, then ephmeral Curve25519 keys
|
||||
// are generated and used with recipient's public key.
|
||||
func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||
|
||||
if blksize >= uint64(maxChunkSize) {
|
||||
return nil, fmt.Errorf("encrypt: Blocksize is too large (max 16M)")
|
||||
}
|
||||
|
@ -131,61 +136,6 @@ func (e *Encryptor) AddRecipient(pk *PublicKey) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Begin the encryption process by writing the header
|
||||
func (e *Encryptor) start(wr io.Writer) error {
|
||||
varHdrLen := sha256.Size + e.Size()
|
||||
|
||||
hdrBuf := make([]byte, _FixedHdrLen + varHdrLen)
|
||||
|
||||
fixedHdr := hdrBuf[:_FixedHdrLen]
|
||||
varHdr := hdrBuf[_FixedHdrLen:]
|
||||
|
||||
cksum := varHdr[:sha256.Size]
|
||||
pbHdr := varHdr[sha256.Size:]
|
||||
|
||||
// Now assemble the fixed header
|
||||
copy(fixedHdr[:], []byte(_Magic))
|
||||
fixedHdr[_MagicLen] = 1 // version #
|
||||
binary.BigEndian.PutUint32(fixedHdr[_MagicLen+1:], uint32(varHdrLen))
|
||||
|
||||
// Now marshal the variable portion
|
||||
_, err := e.MarshalToSizedBuffer(pbHdr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: can't marshal header: %s", err)
|
||||
}
|
||||
|
||||
// Now calculate checksum of everything
|
||||
h := sha256.New()
|
||||
h.Write(fixedHdr)
|
||||
h.Write(pbHdr)
|
||||
h.Sum(cksum[:0])
|
||||
|
||||
// Finally write it out
|
||||
err = fullwrite(hdrBuf, wr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %s", err)
|
||||
}
|
||||
|
||||
e.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write _all_ bytes of buffer 'buf'
|
||||
func fullwrite(buf []byte, wr io.Writer) error {
|
||||
n := len(buf)
|
||||
|
||||
for n > 0 {
|
||||
m, err := wr.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("I/O error: %s", err)
|
||||
}
|
||||
|
||||
n -= m
|
||||
buf = buf[m:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encrypt the input stream 'rd' and write encrypted stream to 'wr'
|
||||
func (e *Encryptor) Encrypt(rd io.Reader, wr io.Writer) error {
|
||||
if !e.started {
|
||||
|
@ -219,6 +169,57 @@ func (e *Encryptor) Encrypt(rd io.Reader, wr io.Writer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Begin the encryption process by writing the header
|
||||
func (e *Encryptor) start(wr io.Writer) error {
|
||||
varSize := e.Size()
|
||||
|
||||
buffer := make([]byte, _FixedHdrLen+varSize+sha256.Size)
|
||||
fixHdr := buffer[:_FixedHdrLen]
|
||||
varHdr := buffer[_FixedHdrLen:]
|
||||
sumHdr := varHdr[varSize:]
|
||||
|
||||
// Now assemble the fixed header
|
||||
copy(fixHdr[:], []byte(_Magic))
|
||||
fixHdr[_MagicLen] = 1 // version #
|
||||
binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize))
|
||||
|
||||
// Now marshal the variable portion
|
||||
_, err := e.MarshalToSizedBuffer(varHdr[:varSize])
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: can't marshal header: %s", err)
|
||||
}
|
||||
|
||||
// Now calculate checksum of everything
|
||||
h := sha256.New()
|
||||
h.Write(buffer[:_FixedHdrLen+varSize])
|
||||
h.Sum(sumHdr[:0])
|
||||
|
||||
// Finally write it out
|
||||
err = fullwrite(buffer, wr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt: %s", err)
|
||||
}
|
||||
|
||||
e.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write _all_ bytes of buffer 'buf'
|
||||
func fullwrite(buf []byte, wr io.Writer) error {
|
||||
n := len(buf)
|
||||
|
||||
for n > 0 {
|
||||
m, err := wr.Write(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("I/O error: %s", err)
|
||||
}
|
||||
|
||||
n -= m
|
||||
buf = buf[m:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encrypt exactly _one_ block of data
|
||||
// The nonce for the block is: sha256(salt || chunkLen || block#)
|
||||
// This protects the output stream from re-ordering attacks and length
|
||||
|
@ -231,7 +232,7 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error
|
|||
|
||||
// mark last block
|
||||
if eof {
|
||||
z |= EOF
|
||||
z |= _EOF
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(b[:4], z)
|
||||
|
@ -285,29 +286,29 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
|||
return nil, fmt.Errorf("decrypt: Unsupported version %d", b[_MagicLen])
|
||||
}
|
||||
|
||||
hdrlen := binary.BigEndian.Uint32(b[_MagicLen+1:])
|
||||
varSize := binary.BigEndian.Uint32(b[_MagicLen+1:])
|
||||
|
||||
// sanity check on variable segment length
|
||||
if hdrlen > 1048576 {
|
||||
if varSize > 1048576 {
|
||||
return nil, fmt.Errorf("decrypt: header too large (max 1048576)")
|
||||
}
|
||||
if hdrlen < 32 {
|
||||
if varSize < 32 {
|
||||
return nil, fmt.Errorf("decrypt: header too small (min 32)")
|
||||
}
|
||||
|
||||
hdr := make([]byte, hdrlen)
|
||||
// SHA256 is the trailer part of the file-header
|
||||
varBuf := make([]byte, varSize+sha256.Size)
|
||||
|
||||
_, err = io.ReadFull(rd, hdr)
|
||||
_, err = io.ReadFull(rd, varBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt: err while reading header: %s", err)
|
||||
}
|
||||
|
||||
verify := hdr[:sha256.Size]
|
||||
pbHdr := hdr[sha256.Size:]
|
||||
verify := varBuf[varSize:]
|
||||
|
||||
h := sha256.New()
|
||||
h.Write(b[:])
|
||||
h.Write(pbHdr)
|
||||
h.Write(varBuf[:varSize])
|
||||
cksum := h.Sum(nil)
|
||||
|
||||
if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 {
|
||||
|
@ -318,7 +319,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
|||
rd: rd,
|
||||
}
|
||||
|
||||
err = d.Header.Unmarshal(pbHdr)
|
||||
err = d.Header.Unmarshal(varBuf[:varSize])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt: decode error: %s", err)
|
||||
}
|
||||
|
@ -429,7 +430,7 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
|||
|
||||
n, err := io.ReadFull(d.rd, b[:4])
|
||||
if err == io.EOF || err == io.ErrClosedPipe || n == 0 {
|
||||
return nil, false, fmt.Errorf("decrypt: premature EOF-1 while reading block %d", i)
|
||||
return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -437,12 +438,12 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
|||
}
|
||||
|
||||
m := binary.BigEndian.Uint32(b[:4])
|
||||
eof := (m & EOF) > 0
|
||||
eof := (m & _EOF) > 0
|
||||
|
||||
m &= (EOF-1)
|
||||
m &= (_EOF - 1)
|
||||
|
||||
// Sanity check - in case of corrupt header
|
||||
if m > (uint32(d.ae.Overhead()) + chunkSize) {
|
||||
if m > (uint32(d.ae.Overhead()) + d.ChunkSize) {
|
||||
return nil, false, fmt.Errorf("decrypt: chunksize is too large (%d)", m)
|
||||
}
|
||||
|
||||
|
@ -456,7 +457,7 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
|||
if m > 0 {
|
||||
n, err = io.ReadFull(d.rd, d.buf[:m])
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("decrypt: premature EOF-2 while reading block %d: %s", i, err)
|
||||
return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err)
|
||||
}
|
||||
|
||||
p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:])
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
)
|
||||
|
||||
// one sender, one receiver no verification of sender
|
||||
func TestSimple(t *testing.T) {
|
||||
func TestEncryptSimple(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
|
||||
receiver, err := NewKeypair()
|
||||
|
@ -66,7 +66,7 @@ func TestSimple(t *testing.T) {
|
|||
}
|
||||
|
||||
// test corrupted header or corrupted input
|
||||
func TestCorrupted(t *testing.T) {
|
||||
func TestEncryptCorrupted(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
|
||||
receiver, err := NewKeypair()
|
||||
|
@ -105,7 +105,7 @@ func TestCorrupted(t *testing.T) {
|
|||
}
|
||||
|
||||
// one sender, one receiver with verification of sender
|
||||
func TestSenderVerified(t *testing.T) {
|
||||
func TestEncryptSenderVerified(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
|
||||
sender, err := NewKeypair()
|
||||
|
@ -151,7 +151,7 @@ func TestSenderVerified(t *testing.T) {
|
|||
}
|
||||
|
||||
// one sender, multiple receivers, each decrypting the blob
|
||||
func TestMultiReceiver(t *testing.T) {
|
||||
func TestEncryptMultiReceiver(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
|
||||
sender, err := NewKeypair()
|
163
sign/sign.go
163
sign/sign.go
|
@ -22,6 +22,8 @@ package sign
|
|||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
|
@ -77,50 +79,38 @@ type Signature struct {
|
|||
pkhash []byte // [0:16] SHA256 hash of public key needed for verification
|
||||
}
|
||||
|
||||
// Algorithm used in the encrypted private key
|
||||
const sk_algo = "scrypt-sha256"
|
||||
const sig_algo = "sha512-ed25519"
|
||||
|
||||
// Length of Ed25519 Public Key Hash
|
||||
const PKHashLength = 16
|
||||
|
||||
const (
|
||||
// Scrypt parameters
|
||||
const _N = 1 << 17
|
||||
const _r = 16
|
||||
const _p = 1
|
||||
_N int = 1 << 19
|
||||
_r int = 8
|
||||
_p int = 1
|
||||
|
||||
// Algorithm used in the encrypted private key
|
||||
sk_algo = "scrypt-sha256"
|
||||
sig_algo = "sha512-ed25519"
|
||||
)
|
||||
|
||||
// Encrypted Private key
|
||||
type encPrivKey struct {
|
||||
// Encrypted Sk
|
||||
Esk []byte
|
||||
type serializedPrivKey struct {
|
||||
Comment string `yaml:"comment,omitempty"`
|
||||
|
||||
// parameters for Sk serialization
|
||||
Salt []byte
|
||||
// Encrypted Sk
|
||||
Esk string `yaml:"esk"`
|
||||
Salt string `yaml:"salt,omitempty"`
|
||||
|
||||
// Algorithm used for checksum and KDF
|
||||
Algo string
|
||||
|
||||
// Checksum to verify passphrase before we xor it
|
||||
Verify []byte
|
||||
Algo string `yaml:"algo,omitempty"`
|
||||
|
||||
// These are params for scrypt.Key()
|
||||
// CPU Cost parameter; must be a power of 2
|
||||
N uint32
|
||||
// r * p should be less than 2^30
|
||||
r uint32
|
||||
p uint32
|
||||
}
|
||||
N int `yaml:"Z,flow,omitempty"`
|
||||
|
||||
// Serialized representation of private key
|
||||
type serializedPrivKey struct {
|
||||
Comment string `yaml:"comment,omitempty"`
|
||||
Esk string `yaml:"esk"`
|
||||
Salt string `yaml:"salt,omitempty"`
|
||||
Algo string `yaml:"algo,omitempty"`
|
||||
Verify string `yaml:"verify,omitempty"`
|
||||
N uint32 `yaml:"Z,flow,omitempty"`
|
||||
R uint32 `yaml:"r,flow,omitempty"`
|
||||
P uint32 `yaml:"p,flow,omitempty"`
|
||||
// r * p should be less than 2^30
|
||||
R int `yaml:"r,flow,omitempty"`
|
||||
P int `yaml:"p,flow,omitempty"`
|
||||
}
|
||||
|
||||
// serialized representation of public key
|
||||
|
@ -213,48 +203,43 @@ func MakePrivateKey(yml []byte, pw []byte) (*PrivateKey, error) {
|
|||
|
||||
err := yaml.Unmarshal(yml, &ssk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse YAML: %s", err)
|
||||
return nil, fmt.Errorf("make priv key: can't parse YAML: %s", err)
|
||||
}
|
||||
|
||||
esk := &encPrivKey{N: ssk.N, r: ssk.R, p: ssk.P, Algo: ssk.Algo}
|
||||
b64 := base64.StdEncoding.DecodeString
|
||||
|
||||
esk.Esk, err = b64(ssk.Esk)
|
||||
salt, err := b64(ssk.Salt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't decode YAML:Esk: %s", err)
|
||||
return nil, fmt.Errorf("make priv key: can't decode salt: %s", err)
|
||||
}
|
||||
|
||||
esk.Salt, err = b64(ssk.Salt)
|
||||
esk, err := b64(ssk.Esk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't decode YAML:Salt: %s", err)
|
||||
}
|
||||
|
||||
esk.Verify, err = b64(ssk.Verify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't decode YAML:Verify: %s", err)
|
||||
return nil, fmt.Errorf("make priv key: can't decode key: %s", err)
|
||||
}
|
||||
|
||||
// We take short passwords and extend them
|
||||
pwb := sha512.Sum512(pw)
|
||||
|
||||
xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(esk.Esk))
|
||||
// "32" == Length of AES-256 key
|
||||
key, err := scrypt.Key(pwb[:], salt, ssk.N, ssk.R, ssk.P, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't derive key: %s", err)
|
||||
return nil, fmt.Errorf("make priv key: can't derive key: %s", err)
|
||||
}
|
||||
|
||||
hh := sha256.New()
|
||||
hh.Write(esk.Salt)
|
||||
hh.Write(xork)
|
||||
ck := hh.Sum(nil)
|
||||
|
||||
if subtle.ConstantTimeCompare(esk.Verify, ck) != 1 {
|
||||
return nil, fmt.Errorf("incorrect private key password")
|
||||
aes, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
|
||||
}
|
||||
|
||||
// Everything works. Now, decode the key
|
||||
skb := make([]byte, len(esk.Esk))
|
||||
for i := 0; i < len(esk.Esk); i++ {
|
||||
skb[i] = esk.Esk[i] ^ xork[i]
|
||||
ae, err := cipher.NewGCM(aes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("make priv key: aes failure: %s", err)
|
||||
}
|
||||
|
||||
skb, err := ae.Open(nil, salt[:ae.NonceSize()], esk, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("make priv key: wrong password")
|
||||
}
|
||||
|
||||
return PrivateKeyFromBytes(skb)
|
||||
|
@ -295,60 +280,58 @@ func (pk *PublicKey) Hash() []byte {
|
|||
}
|
||||
|
||||
// Serialize the private key to a file
|
||||
// AEAD encryption for protecting the private key
|
||||
// Format: YAML
|
||||
// All []byte are in base64 (RawEncoding)
|
||||
func (sk *PrivateKey) serialize(fn, comment string, getpw func() ([]byte, error)) error {
|
||||
|
||||
pw, err := getpw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString
|
||||
esk := &encPrivKey{}
|
||||
ssk := &serializedPrivKey{Comment: comment}
|
||||
|
||||
// expand the password into 64 bytes
|
||||
pwb := sha512.Sum512(pw)
|
||||
pass := sha512.Sum512(pw)
|
||||
salt := make([]byte, 32)
|
||||
|
||||
esk.N = _N
|
||||
esk.r = _r
|
||||
esk.p = _p
|
||||
randread(salt)
|
||||
|
||||
esk.Salt = make([]byte, 32)
|
||||
esk.Esk = make([]byte, len(sk.Sk))
|
||||
|
||||
randread(esk.Salt)
|
||||
xork, err := scrypt.Key(pwb[:], esk.Salt, int(esk.N), int(esk.r), int(esk.p), len(sk.Sk))
|
||||
// "32" == Length of AES-256 key
|
||||
key, err := scrypt.Key(pass[:], salt, _N, _r, _p, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Can't derive scrypt key: %s", err)
|
||||
return fmt.Errorf("marshal: can't derive scrypt key: %s", err)
|
||||
}
|
||||
|
||||
hh := sha256.New()
|
||||
hh.Write(esk.Salt)
|
||||
hh.Write(xork)
|
||||
esk.Verify = hh.Sum(nil)
|
||||
aes, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %s", err)
|
||||
}
|
||||
|
||||
ae, err := cipher.NewGCM(aes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal: %s", err)
|
||||
}
|
||||
|
||||
tl := ae.Overhead()
|
||||
buf := make([]byte, tl+len(sk.Sk))
|
||||
esk := ae.Seal(buf[:0], salt[:ae.NonceSize()], sk.Sk, nil)
|
||||
|
||||
enc := base64.StdEncoding.EncodeToString
|
||||
|
||||
ssk := serializedPrivKey{
|
||||
Comment: comment,
|
||||
Esk: enc(esk),
|
||||
Salt: enc(salt),
|
||||
Algo: sk_algo,
|
||||
N: _N,
|
||||
R: _r,
|
||||
P: _p,
|
||||
}
|
||||
|
||||
// We won't protect the Scrypt parameters with the hash above
|
||||
// because it is not needed. If the parameters are wrong, the
|
||||
// derived key will be wrong and thus, the hash will not match.
|
||||
|
||||
esk.Algo = sk_algo // global var
|
||||
|
||||
// Finally setup the encrypted key
|
||||
for i := 0; i < len(sk.Sk); i++ {
|
||||
esk.Esk[i] = sk.Sk[i] ^ xork[i]
|
||||
}
|
||||
|
||||
ssk.Esk = b64(esk.Esk)
|
||||
ssk.Salt = b64(esk.Salt)
|
||||
ssk.Verify = b64(esk.Verify)
|
||||
ssk.Algo = esk.Algo
|
||||
ssk.N = esk.N
|
||||
ssk.R = esk.r
|
||||
ssk.P = esk.p
|
||||
|
||||
out, err := yaml.Marshal(ssk)
|
||||
out, err := yaml.Marshal(&ssk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't marahal to YAML: %s", err)
|
||||
}
|
||||
|
|
|
@ -14,39 +14,13 @@
|
|||
package sign
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"testing"
|
||||
// module under test
|
||||
//"github.com/sign"
|
||||
)
|
||||
|
||||
func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) {
|
||||
return func(cond bool, msg string, args ...interface{}) {
|
||||
if cond {
|
||||
return
|
||||
}
|
||||
|
||||
_, file, line, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 0
|
||||
}
|
||||
|
||||
s := fmt.Sprintf(msg, args...)
|
||||
t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if two byte arrays are equal
|
||||
func byteEq(x, y []byte) bool {
|
||||
return subtle.ConstantTimeCompare(x, y) == 1
|
||||
}
|
||||
|
||||
// Return a temp dir in a temp-dir
|
||||
func tempdir(t *testing.T) string {
|
||||
assert := newAsserter(t)
|
||||
|
@ -103,7 +77,7 @@ p: 1
|
|||
`
|
||||
|
||||
// #1. Create new key pair, and read them back.
|
||||
func Test0(t *testing.T) {
|
||||
func TestSignSimple(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
|
||||
kp, err := NewKeypair()
|
||||
|
@ -154,7 +128,7 @@ func Test0(t *testing.T) {
|
|||
}
|
||||
|
||||
// #2. Create new key pair, sign a rand buffer and verify
|
||||
func Test1(t *testing.T) {
|
||||
func TestSignRandBuf(t *testing.T) {
|
||||
assert := newAsserter(t)
|
||||
kp, err := NewKeypair()
|
||||
assert(err == nil, "NewKeyPair() fail")
|
||||
|
|
43
sign/utils_test.go
Normal file
43
sign/utils_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// utils_test.go -- Test harness utilities for sign
|
||||
//
|
||||
// (c) 2016 Sudhi Herle <sudhi@herle.net>
|
||||
//
|
||||
// Licensing Terms: GPLv2
|
||||
//
|
||||
// If you need a commercial license for this work, please contact
|
||||
// the author.
|
||||
//
|
||||
// This software does not come with any express or implied
|
||||
// warranty; it is provided "as is". No claim is made to its
|
||||
// suitability for any purpose.
|
||||
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newAsserter(t *testing.T) func(cond bool, msg string, args ...interface{}) {
|
||||
return func(cond bool, msg string, args ...interface{}) {
|
||||
if cond {
|
||||
return
|
||||
}
|
||||
|
||||
_, file, line, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 0
|
||||
}
|
||||
|
||||
s := fmt.Sprintf(msg, args...)
|
||||
t.Fatalf("%s: %d: Assertion failed: %s\n", file, line, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if two byte arrays are equal
|
||||
func byteEq(x, y []byte) bool {
|
||||
return subtle.ConstantTimeCompare(x, y) == 1
|
||||
}
|
2
version
2
version
|
@ -1 +1 @@
|
|||
0.6.2
|
||||
0.7.0
|
||||
|
|
Loading…
Add table
Reference in a new issue