From 945046a815bd378ed351c2e651e1c6e3c91666d4 Mon Sep 17 00:00:00 2001 From: Sudhi Herle Date: Sat, 15 May 2021 19:35:54 -0700 Subject: [PATCH] v2 of sigtool with some changes: - aead nonce construction is efficient (replace last 8 bytes of salt with encoded block# and chunk-size - increase aead nonce size to 32 bytes - refactor errors into a separate file - update "build" to latest version - updated README. --- README.md | 74 ++++----- build | 37 +++-- sign/encrypt.go | 417 +++++++++++++++++++++++++----------------------- sign/errors.go | 43 +++++ sign/ssh.go | 12 -- sign/stream.go | 10 +- version | 1 - 7 files changed, 325 insertions(+), 269 deletions(-) create mode 100644 sign/errors.go delete mode 100644 version diff --git a/README.md b/README.md index 493bcd7..be53378 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,26 @@ `sigtool` is an opinionated tool to generate keys, sign, verify, encrypt & decrypt files using Ed25519 signature scheme. In many ways, it is like like OpenBSD's [signify][1] -- except written in Golang and definitely -easier to use. +easier to use. It can use SSH ed25519 public and private keys. It can sign and verify very large files - it prehashes the files with SHA-512 and then signs the SHA-512 checksum. The keys and signatures -are YAML files and so, human readable. +are human readable YAML files. It can encrypt files for multiple recipients - each of whom is identified -by their Ed25519 public key. The encryption by default generates ephmeral +by their Ed25519 public key. The encryption generates ephmeral Curve25519 keys and creates pair-wise shared secret for each recipient of -the encrypted file. The caller can optionally use a specific secret key +the encrypted file. The caller can optionally use a specific private key during the encryption process - this has the benefit of also authenticating the sender (and the receiver can verify the sender if they possess the -corresponding public key). +corresponding sender's public key). The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys *or* the keys generated by sigtool. This means, you can send encrypted files to any recipient identified by their comment in `~/.ssh/authorized_keys`. ## How do I build it? -With Go 1.5 and later: +With Go 1.13 and later: git clone https://github.com/opencoff/sigtool cd sigtool @@ -144,15 +144,21 @@ a random 32-byte AES-256 key. This key is mixed in with the header checksum as a safeguard to protect the header against accidental or malicious corruption. The input is broken into chunks and each chunk is individually AEAD encrypted. The default chunk size 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. +its own nonce from a global salt. The nonce is calculated as follows: -### What is the public-key cryptography? + - v1: SHA256 of the salt, the chunk length and the block number. + - v2: Last 8 bytes of a 32-byte salt is the big-endian encoding of + the chunk-length and block number + +The last block has its most-signficant-bit set to 1 to denote EOF. Thus, the +maximum chunk size is set to 1GB. + +### What is the public-key cryptography in sigtool? `sigtool` uses ephemeral Curve25519 keys to generate shared secrets between pairs of sender & one or more recipients. This pairwise shared -secret is used as a key-encryption-key (KEK) to encrypt the +secret is used as a key-encryption-key (KEK) to wrap the data-encryption key in AEAD mode. Thus, each recipient has their own -individual encrypted key blob. +individual encrypted key blob - that **only** they can decrypt. If the sender authenticates the encryption by providing their secret key, the data-encryption key is signed via Ed25519 and the signature @@ -161,7 +167,7 @@ header. If the sender opts to not authenticate, a "signature" of all zeroes is encrypted instead. The Ed25519 keys generated by `sigtool` or OpenSSH are transformed to their -corresponding Curve25519 points in order to generate the shared secret. +corresponding Curve25519 points in order to generate the pair-wise shared secret. This elliptic co-ordinate transform follows [FiloSottile's writeup][2]. ### Format of the Encrypted File @@ -205,19 +211,33 @@ chunk is encoded the same way: ```C 4 byte chunk length (big endian encoding) - encrypted chunk data + AEAD encrypted chunk data AEAD tag ``` The chunk length does _not_ include the AEAD tag length; it is implicitly -computed. - -The chunk data and AEAD tag are treated as an atomic unit for AEAD +computed. The chunk data and AEAD tag are treated as an atomic unit for AEAD decryption. ### How is the private key protected? The Ed25519 private key is encrypted in AES-GCM-256 mode using a key -derived from the user's pass-phrase. +derived from the user's pass-phrase. The user pass phrase is expanded via +SHA256; this expanded pass phrase is fed to `scrypt()` to +generate a key-encryption-key. In pseudo code, this operation looks +like below: + + passphrase = get_user_passphrase() + expanded = SHA512(passphrase) + salt = randombytes(32) + key = Scrypt(expanded, salt, N, r, p) + esk = AES256_GCM(ed25519_private_key, key) + +Where, ```N```, ```r```, ```p``` are Scrypt parameters. In our +implementation: + + N = 2^19 (1 << 19) + r = 8 + p = 1 ## Understanding the Code @@ -230,7 +250,7 @@ etc. * `src/keys.go` contains key generation, serialization, de-serialization * `src/ssh.go` contains code to parse SSH Ed25519 key files * `src/stream.go` contains code that provides an `io.Reader` and `io.WriteCloser` interface - for encryption and decryption. + for encryption and decryption. The generated keys and signatures are proper YAML files and human readable. @@ -263,24 +283,6 @@ And, a serialized Ed25519 private key looks like so: p: 1 ``` -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) - 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 = 2^19 (1 << 19) - r = 8 - p = 1 ### Ed25519 Signature A generated signature looks like below after serialization: diff --git a/build b/build index 564ce0b..cca95b1 100755 --- a/build +++ b/build @@ -27,14 +27,12 @@ PWD=`pwd` Static=0 Dryrun=0 -Prodver=0.1 +Prodver="" Verbose=0 hostos=$(go env GOHOSTOS) || exit 1 hostcpu=$(go env GOHOSTARCH) || exit 1 -[ -f ./version ] && Prodver=$(cat ./version) - die() { echo "$Z: $@" 1>&2 exit 0 @@ -102,7 +100,8 @@ And, PROGS is one or more go programs. With no arguments, $0 builds: $Progs (source in ./src/) -If ./version is present, its content are used as version number for the binary. +The repository's latest tag is used as the default version of the software being +built. Options: -h, --help Show this help message and quit @@ -153,6 +152,7 @@ done Tool= doinit=0 args= +Printarch=0 #set -x ac_prev= @@ -214,6 +214,10 @@ do set -x ;; + --print-arch) + Printarch=1 + ;; + *) # first non option terminates option processing. # we gather all remaining args and bundle them up. args="$args $ac_option" @@ -228,7 +232,7 @@ done [ $Dryrun -gt 0 ] && e=echo # let every error abort -#set -e +set -e # This fragment can't be in a function - since it exports several vars if [ -n "$Arch" ]; then @@ -274,6 +278,12 @@ else fi fi +if [ $Printarch -gt 0 ]; then + echo "$hostos-$hostcpu" + exit 0 +fi + + # This is where build outputs go Bindir=$PWD/bin/$cross Hostbindir=$PWD/bin/$hostos-$hostcpu @@ -290,21 +300,28 @@ if [ -d "./.hg" ]; then else rev="hg:${brev}" fi + if [ -z "$Prodver" ]; then + Prodver=$(hg log -r "branch(stable) and tag()" -T "{tags}\n" | tail -1) + fi elif [ -d "./.git" ]; then xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1 rev="git:$xrev" + if [ -z "$Prodver" ]; then + Prodver=$(git tag --list | tail -1) + fi else rev="UNKNOWN-VER" echo "$0: Can't find version info" 1>&2 fi + # Do Protobufs if needed if [ -n "$Protobufs" ]; then slick=$Hostbindir/protoc-gen-gogoslick slicksrc=github.com/gogo/protobuf/protoc-gen-gogoslick pc=$(type -p protoc) - [ -z "$pc" ] && die "Please install protobuf-tools .." + [ -z "$pc" ] && die "Need 'protoc' for building .." slick=$(hosttool protoc-gen-gogoslick $Hostbindir $slicksrc) || exit 1 #if [ ! -f $slick ]; then @@ -312,17 +329,15 @@ if [ -n "$Protobufs" ]; then # $e go build -o $slick github.com/gogo/protobuf/protoc-gen-gogoslick || exit 1 #i - PATH=$Hostbindir:$PATH - export PATH + export PATH=$PATH:$Hostbindir for f in $Protobufs; do dn=$(dirname $f) bn=$(basename $f .proto) of=$dn/${bn}.pb.go if [ $f -nt $of ]; then - echo "gogoslick: $f -> $of ..." + echo "Running $pc .." $e $pc --gogoslick_out=. $f || exit 1 - $e gofmt -w $of fi done fi @@ -354,7 +369,7 @@ case $Tool in all="$@" fi - echo "Building $msg $Prodver ($rev) for $cross .." + echo "Building $Prodver ($rev), $cross $msg .." for p in $all; do if echo $p | grep -q ':' ; then diff --git a/sign/encrypt.go b/sign/encrypt.go index 23411c5..2e768db 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -42,7 +42,7 @@ // The input data is broken up into "chunks"; each no larger than // maxChunkSize. The default block size is "chunkSize". Each block // is AEAD encrypted: -// AEAD nonce = SHA256(header.salt || block# || block-size) +// AEAD nonce = header.salt || block# || block-size // // The encrypted block (includes the AEAD tag) length is written // as a big-endian 4-byte prefix. The high-order bit of this length @@ -72,13 +72,14 @@ import ( // Encryption chunk size = 4MB const ( chunkSize uint32 = 4 * 1048576 - maxChunkSize uint32 = 16 * 1048576 + maxChunkSize uint32 = 1 << 30 _EOF uint32 = 1 << 31 - _Magic = "SigTool" - _MagicLen = len(_Magic) - _AEADNonceLen = 16 - _FixedHdrLen = _MagicLen + 1 + 4 + _Magic = "SigTool" + _MagicLen = len(_Magic) + _SigtoolVersion = 2 + _AEADNonceLen = 32 + _FixedHdrLen = _MagicLen + 1 + 4 _WrapReceiverNonce = "Receiver Key Nonce" _WrapSenderNonce = "Sender Sig Nonce" @@ -119,7 +120,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) { // generate ephemeral Curve25519 keys esk, epk, err := newSender() if err != nil { - return nil, fmt.Errorf("encrypt: %s", err) + return nil, fmt.Errorf("encrypt: %w", err) } key := make([]byte, 32) @@ -134,7 +135,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) { if sk != nil { sig, err := sk.SignMessage(key, "") if err != nil { - return nil, fmt.Errorf("encrypt: can't sign: %s", err) + return nil, fmt.Errorf("encrypt: can't sign: %w", err) } senderSig = sig.Sig @@ -145,7 +146,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) { wSig, err := wrapSenderSig(senderSig, key, salt) if err != nil { - return nil, fmt.Errorf("encrypt: %s", err) + return nil, fmt.Errorf("encrypt: %w", err) } e := &Encryptor{ @@ -166,7 +167,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) { // Add a new recipient to this encryption context. func (e *Encryptor) AddRecipient(pk *PublicKey) error { if e.started { - return fmt.Errorf("encrypt: can't add new recipient after encryption has started") + return ErrEncStarted } w, err := e.wrapKey(pk) @@ -180,7 +181,7 @@ func (e *Encryptor) AddRecipient(pk *PublicKey) error { // Encrypt the input stream 'rd' and write encrypted stream to 'wr' func (e *Encryptor) Encrypt(rd io.Reader, wr io.WriteCloser) error { if e.stream { - return fmt.Errorf("encrypt: can't use Encrypt() after using streaming I/O") + return ErrEncIsStream } if !e.started { @@ -201,7 +202,7 @@ func (e *Encryptor) Encrypt(rd io.Reader, wr io.WriteCloser) error { case io.EOF, io.ErrClosedPipe, io.ErrUnexpectedEOF: eof = true default: - return fmt.Errorf("encrypt: I/O read error: %s", err) + return fmt.Errorf("encrypt: I/O read error: %w", err) } } @@ -229,13 +230,13 @@ func (e *Encryptor) start(wr io.Writer) error { // Now assemble the fixed header copy(fixHdr[:], []byte(_Magic)) - fixHdr[_MagicLen] = 1 // version # + fixHdr[_MagicLen] = _SigtoolVersion binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize)) // Now marshal the variable portion _, err := e.MarshalTo(varHdr[:varSize]) if err != nil { - return fmt.Errorf("encrypt: can't marshal header: %s", err) + return fmt.Errorf("encrypt: can't marshal header: %w", err) } // Now calculate checksum of everything @@ -246,7 +247,7 @@ func (e *Encryptor) start(wr io.Writer) error { // Finally write it out err = fullwrite(buffer, wr) if err != nil { - return fmt.Errorf("encrypt: %s", err) + return fmt.Errorf("encrypt: %w", err) } // we mix the header checksum to create the encryption key @@ -258,12 +259,12 @@ func (e *Encryptor) start(wr io.Writer) error { aes, err := aes.NewCipher(key) if err != nil { - return fmt.Errorf("encrypt: %s", err) + return fmt.Errorf("encrypt: %w", err) } ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen) if err != nil { - return fmt.Errorf("encrypt: %s", err) + return fmt.Errorf("encrypt: %w", err) } e.buf = make([]byte, e.ChunkSize+4+uint32(ae.Overhead())) @@ -273,30 +274,14 @@ func (e *Encryptor) start(wr io.Writer) error { 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 // modification attacks. The encoded length & block number is used as // additional data in the AEAD construction. func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error { - var nonceb [32]byte var z uint32 = uint32(len(buf)) + var nbuf [_AEADNonceLen]byte // mark last block if eof { @@ -307,10 +292,7 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error binary.BigEndian.PutUint32(b[:4], z) binary.BigEndian.PutUint32(b[4:], i) - h := sha256.New() - h.Write(e.Salt) - h.Write(b[:]) - nonce := h.Sum(nonceb[:0])[:e.ae.NonceSize()] + nonce := makeNonceV2(nbuf[:], e.Salt, b) cbuf := e.buf[4:] c := e.ae.Seal(cbuf[:0], nonce, buf, b[:]) @@ -319,7 +301,7 @@ func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error n := len(c) + 4 err := fullwrite(e.buf[:n], wr) if err != nil { - return fmt.Errorf("encrypt: %s", err) + return fmt.Errorf("encrypt: %w", err) } return nil } @@ -349,25 +331,27 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { _, err := io.ReadFull(rd, b[:]) if err != nil { - return nil, fmt.Errorf("decrypt: err while reading header: %s", err) + return nil, fmt.Errorf("decrypt: err while reading header: %w", err) } if bytes.Compare(b[:_MagicLen], []byte(_Magic)) != 0 { - return nil, fmt.Errorf("decrypt: Not a sigtool encrypted file?") + return nil, ErrNotSigTool } - if b[_MagicLen] != 1 { - return nil, fmt.Errorf("decrypt: Unsupported version %d", b[_MagicLen]) + // Version check + if b[_MagicLen] != _SigtoolVersion { + return nil, fmt.Errorf("decrypt: Unsupported version %d; this tool only supports v%d", + b[_MagicLen], _SigtoolVersion) } varSize := binary.BigEndian.Uint32(b[_MagicLen+1:]) // sanity check on variable segment length if varSize > 1048576 { - return nil, fmt.Errorf("decrypt: header too large (max 1048576)") + return nil, ErrHeaderTooBig } if varSize < 32 { - return nil, fmt.Errorf("decrypt: header too small (min 32)") + return nil, ErrHeaderTooSmall } // SHA256 is the trailer part of the file-header @@ -375,7 +359,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { _, err = io.ReadFull(rd, varBuf) if err != nil { - return nil, fmt.Errorf("decrypt: err while reading header: %s", err) + return nil, fmt.Errorf("decrypt: error while reading header: %w", err) } verify := varBuf[varSize:] @@ -386,7 +370,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { cksum := h.Sum(nil) if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 { - return nil, fmt.Errorf("decrypt: header corrupted") + return nil, ErrBadHeader } d := &Decryptor{ @@ -396,7 +380,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { err = d.Unmarshal(varBuf[:varSize]) if err != nil { - return nil, fmt.Errorf("decrypt: decode error: %s", err) + return nil, fmt.Errorf("decrypt: decode error: %w", err) } if d.ChunkSize == 0 || d.ChunkSize >= maxChunkSize { @@ -408,7 +392,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) { } if len(d.Keys) == 0 { - return nil, fmt.Errorf("decrypt: no wrapped keys") + return nil, ErrNoWrappedKeys } // sanity check on the wrapped keys @@ -430,18 +414,18 @@ func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error { for i, w := range d.Keys { key, err = d.unwrapKey(w, sk) if err != nil { - return fmt.Errorf("decrypt: can't unwrap key %d: %s", i, err) + return fmt.Errorf("decrypt: can't unwrap key %d: %w", i, err) } if key != nil { goto havekey } } - return fmt.Errorf("decrypt: wrong key") + return ErrBadKey havekey: if err := d.verifySender(key, sk, senderPk); err != nil { - return fmt.Errorf("decrypt: %s", err) + return fmt.Errorf("decrypt: %w", err) } d.key = key @@ -455,12 +439,12 @@ havekey: aes, err := aes.NewCipher(key) if err != nil { - return fmt.Errorf("decrypt: %s", err) + return fmt.Errorf("decrypt: %w", err) } d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen) if err != nil { - return fmt.Errorf("decrypt: %s", err) + return fmt.Errorf("decrypt: %w", err) } d.buf = make([]byte, int(d.ChunkSize)+d.ae.Overhead()) return nil @@ -475,11 +459,11 @@ func (d *Decryptor) AuthenticatedSender() bool { // Decrypt the file and write to 'wr' func (d *Decryptor) Decrypt(wr io.Writer) error { if d.key == nil { - return fmt.Errorf("decrypt: wrapped-key not decrypted (missing SetPrivateKey()?") + return ErrNoKey } if d.stream { - return fmt.Errorf("decrypt: can't use Decrypt() after using streaming I/O") + return ErrDecStarted } if d.eof { @@ -495,7 +479,7 @@ func (d *Decryptor) Decrypt(wr io.Writer) error { if len(c) > 0 { err = fullwrite(c, wr) if err != nil { - return fmt.Errorf("decrypt: %s", err) + return fmt.Errorf("decrypt: %w", err) } } @@ -507,143 +491,6 @@ func (d *Decryptor) Decrypt(wr io.Writer) error { return nil } -// Wrap sender's signature of the encryption key -func wrapSenderSig(sig []byte, key, salt []byte) ([]byte, error) { - aes, err := aes.NewCipher(key) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - tagsize := ae.Overhead() - nonceSize := ae.NonceSize() - - nonce := makeNonce([]byte(_WrapSenderNonce), salt)[:nonceSize] - esig := make([]byte, tagsize+len(sig)) - - return ae.Seal(esig[:0], nonce, sig, nil), nil -} - -// unwrap sender's signature using 'key' and extract the signature -// Optionally, verify the signature using the sender's PK (if provided). -func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey) error { - aes, err := aes.NewCipher(key) - if err != nil { - return fmt.Errorf("unwrap: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return fmt.Errorf("unwrap: %s", err) - } - - nonceSize := ae.NonceSize() - nonce := makeNonce([]byte(_WrapSenderNonce), d.Salt)[:nonceSize] - sig := make([]byte, ed25519.SignatureSize) - sig, err = ae.Open(sig[:0], nonce, d.SenderSign, nil) - if err != nil { - return fmt.Errorf("unwrap: can't open sender info: %s", err) - } - - var zero [ed25519.SignatureSize]byte - - // Did the sender actually sign anything? - if subtle.ConstantTimeCompare(zero[:], sig) == 0 { - d.auth = true - - if senderPK != nil { - ss := &Signature{ - Sig: sig, - } - - ok := senderPK.VerifyMessage(key, ss) - if !ok { - return fmt.Errorf("unwrap: sender verification failed") - } - } - } - return nil -} - -// Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK -// basically, we do two scalarmults: -// a) Ephemeral encryption/decryption SK x receiver PK -// b) Sender's SK x receiver PK -func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) { - rxPK := pk.toCurve25519PK() - dkek, err := curve25519.X25519(e.encSK, rxPK) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - aes, err := aes.NewCipher(dkek) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - tagsize := ae.Overhead() - nonceSize := ae.NonceSize() - - nonceR := makeNonce([]byte(_WrapReceiverNonce), e.Salt)[:nonceSize] - ekey := make([]byte, tagsize+len(e.key)) - - w := &pb.WrappedKey{ - DKey: ae.Seal(ekey[:0], nonceR, e.key, pk.Pk), - } - - return w, nil -} - -// Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and -// senders ephemeral PublicKey -func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) { - ourSK := sk.toCurve25519SK() - dkek, err := curve25519.X25519(ourSK, d.Pk) - if err != nil { - return nil, fmt.Errorf("unwrap: %s", err) - } - - aes, err := aes.NewCipher(dkek) - if err != nil { - return nil, fmt.Errorf("unwrap: %s", err) - } - - ae, err := cipher.NewGCM(aes) - if err != nil { - return nil, fmt.Errorf("unwrap: %s", err) - } - - // 32 == AES-256 key size - want := 32 + ae.Overhead() - if len(w.DKey) != want { - return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(w.DKey)) - } - - nonceSize := ae.NonceSize() - - nonceR := makeNonce([]byte(_WrapReceiverNonce), d.Salt)[:nonceSize] - pk := sk.PublicKey() - - dkey := make([]byte, 32) // decrypted data decryption key - - // we indicate incorrect receiver SK by returning a nil key - dkey, err = ae.Open(dkey[:0], nonceR, w.DKey, pk.Pk) - if err != nil { - return nil, nil - } - - return dkey, nil -} - // Decrypt exactly one chunk of data func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) { var b [8]byte @@ -679,25 +526,192 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) { } binary.BigEndian.PutUint32(b[4:], i) - h := sha256.New() - h.Write(d.Salt) - h.Write(b[:]) - nonce := h.Sum(nonceb[:0])[:d.ae.NonceSize()] + nonce := makeNonceV2(nonceb[:], d.Salt, b[:]) z := m + ovh n, err = io.ReadFull(d.rd, d.buf[:z]) if err != nil { - return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %s", i, err) + return nil, false, fmt.Errorf("decrypt: premature EOF while reading block %d: %w", i, err) } p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:]) if err != nil { - return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err) + return nil, false, fmt.Errorf("decrypt: can't decrypt chunk %d: %w", i, err) } return p[:m], eof, nil } +// Wrap sender's signature of the encryption key +func wrapSenderSig(sig []byte, key, salt []byte) ([]byte, error) { + aes, err := aes.NewCipher(key) + if err != nil { + return nil, fmt.Errorf("wrap: %w", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return nil, fmt.Errorf("wrap: %w", err) + } + + tagsize := ae.Overhead() + nonceSize := ae.NonceSize() + + nonce := sha256Slices([]byte(_WrapSenderNonce), salt)[:nonceSize] + esig := make([]byte, tagsize+len(sig)) + + return ae.Seal(esig[:0], nonce, sig, nil), nil +} + +// unwrap sender's signature using 'key' and extract the signature +// Optionally, verify the signature using the sender's PK (if provided). +func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey) error { + aes, err := aes.NewCipher(key) + if err != nil { + return fmt.Errorf("unwrap: %w", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return fmt.Errorf("unwrap: %w", err) + } + + nonceSize := ae.NonceSize() + nonce := sha256Slices([]byte(_WrapSenderNonce), d.Salt)[:nonceSize] + sig := make([]byte, ed25519.SignatureSize) + sig, err = ae.Open(sig[:0], nonce, d.SenderSign, nil) + if err != nil { + return fmt.Errorf("unwrap: can't open sender info: %w", err) + } + + var zero [ed25519.SignatureSize]byte + + // Did the sender actually sign anything? + if subtle.ConstantTimeCompare(zero[:], sig) == 0 { + d.auth = true + + if senderPK != nil { + ss := &Signature{ + Sig: sig, + } + + ok := senderPK.VerifyMessage(key, ss) + if !ok { + return fmt.Errorf("unwrap: sender verification failed") + } + } + } + return nil +} + +// Wrap data encryption key 'k' with the sender's PK and our ephemeral curve SK +// basically, we do two scalarmults: +// a) Ephemeral encryption/decryption SK x receiver PK +// b) Sender's SK x receiver PK +func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) { + rxPK := pk.toCurve25519PK() + dkek, err := curve25519.X25519(e.encSK, rxPK) + if err != nil { + return nil, fmt.Errorf("wrap: %w", err) + } + + aes, err := aes.NewCipher(dkek) + if err != nil { + return nil, fmt.Errorf("wrap: %w", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return nil, fmt.Errorf("wrap: %w", err) + } + + tagsize := ae.Overhead() + nonceSize := ae.NonceSize() + + nonceR := sha256Slices([]byte(_WrapReceiverNonce), e.Salt)[:nonceSize] + ekey := make([]byte, tagsize+len(e.key)) + + w := &pb.WrappedKey{ + DKey: ae.Seal(ekey[:0], nonceR, e.key, pk.Pk), + } + + return w, nil +} + +// Unwrap a wrapped key using the receivers Ed25519 secret key 'sk' and +// senders ephemeral PublicKey +func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) { + ourSK := sk.toCurve25519SK() + dkek, err := curve25519.X25519(ourSK, d.Pk) + if err != nil { + return nil, fmt.Errorf("unwrap: %w", err) + } + + aes, err := aes.NewCipher(dkek) + if err != nil { + return nil, fmt.Errorf("unwrap: %w", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return nil, fmt.Errorf("unwrap: %w", err) + } + + // 32 == AES-256 key size + want := 32 + ae.Overhead() + if len(w.DKey) != want { + return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(w.DKey)) + } + + nonceSize := ae.NonceSize() + + nonceR := sha256Slices([]byte(_WrapReceiverNonce), d.Salt)[:nonceSize] + pk := sk.PublicKey() + + dkey := make([]byte, 32) // decrypted data decryption key + + // we indicate incorrect receiver SK by returning a nil key + dkey, err = ae.Open(dkey[:0], nonceR, w.DKey, pk.Pk) + if err != nil { + return nil, nil + } + + return dkey, 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 err + } + + n -= m + buf = buf[m:] + } + return nil +} + +// make aead nonce from salt, chunk-size and block# +func makeNonceV2(dest []byte, salt []byte, ad []byte) []byte { + n := len(ad) + copy(dest, salt[:n]) + copy(dest[n:], ad) + return dest +} + +// make aead nonce from salt, chunk-size and block# for v1 +// This is here for historical documentation purposes +func makeNonceV1(dest []byte, salt []byte, ad []byte) []byte { + h := sha256.New() + h.Write(salt) + h.Write(ad) + return h.Sum(dest[:0]) +} + // generate a KEK from a shared DH key and a Pub Key func expand(shared, pk []byte) ([]byte, error) { kek := make([]byte, 32) @@ -716,7 +730,8 @@ func newSender() (sk, pk []byte, err error) { return } -func makeNonce(v ...[]byte) []byte { +// do sha256 on a list of byte slices +func sha256Slices(v ...[]byte) []byte { h := sha256.New() for _, x := range v { h.Write(x) diff --git a/sign/errors.go b/sign/errors.go new file mode 100644 index 0000000..f039939 --- /dev/null +++ b/sign/errors.go @@ -0,0 +1,43 @@ +// errors.go - list of all exportable errors in this module +// +// (c) 2016 Sudhi Herle +// +// 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 ( + "errors" +) + +var ( + ErrClosed = errors.New("encrypt: stream already closed") + ErrNoKey = errors.New("decrypt: No private key set for decryption") + ErrEncStarted = errors.New("encrypt: can't add new recipient after encryption has started") + ErrDecStarted = errors.New("decrypt: can't add new recipient after decryption has started") + ErrEncIsStream = errors.New("encrypt: can't use Encrypt() after using streaming I/O") + ErrNotSigTool = errors.New("decrypt: Not a sigtool encrypted file?") + ErrHeaderTooBig = errors.New("decrypt: header too large (max 1048576)") + ErrHeaderTooSmall = errors.New("decrypt: header too small (min 32)") + ErrBadHeader = errors.New("decrypt: header corrupted") + ErrNoWrappedKeys = errors.New("decrypt: No wrapped keys in encrypted file") + ErrBadKey = errors.New("decrypt: wrong key") + ErrBadSender = errors.New("unwrap: sender verification failed") + + ErrIncorrectPassword = errors.New("ssh: invalid passphrase") + ErrNoPEMFound = errors.New("ssh: no PEM block found") + ErrBadPublicKey = errors.New("ssh: malformed public key") + ErrKeyTooShort = errors.New("ssh: public key too short") + ErrBadTrailers = errors.New("ssh: trailing junk in public key") + ErrBadFormat = errors.New("ssh: invalid openssh private key format") + ErrBadLength = errors.New("ssh: private key unexpected length") + ErrBadPadding = errors.New("ssh: padding not as expected") +) diff --git a/sign/ssh.go b/sign/ssh.go index 132094c..07655f5 100644 --- a/sign/ssh.go +++ b/sign/ssh.go @@ -25,7 +25,6 @@ import ( "encoding/base64" "encoding/binary" "encoding/pem" - "errors" "fmt" "strings" @@ -34,17 +33,6 @@ import ( "golang.org/x/crypto/ssh" ) -var ( - ErrIncorrectPassword = errors.New("ssh: Invalid Passphrase") - ErrNoPEMFound = errors.New("no PEM block found") - ErrBadPublicKey = errors.New("ssh: malformed public key") - ErrKeyTooShort = errors.New("ssh: public key too short") - ErrBadTrailers = errors.New("ssh: trailing junk in public key") - ErrBadFormat = errors.New("ssh: invalid openssh private key format") - ErrBadLength = errors.New("ssh: private key unexpected length") - ErrBadPadding = errors.New("ssh: padding not as expected") -) - const keySizeAES256 = 32 // ParseEncryptedRawPrivateKey returns a private key from an diff --git a/sign/stream.go b/sign/stream.go index 92e13eb..3304b22 100644 --- a/sign/stream.go +++ b/sign/stream.go @@ -15,8 +15,6 @@ package sign import ( - "errors" - "fmt" "io" ) @@ -98,7 +96,7 @@ func (w *encWriter) Close() error { } w.n = 0 - w.err = errClosed + w.err = ErrClosed return w.wr.Close() } @@ -113,7 +111,7 @@ type encReader struct { // NewStreamReader returns an io.Reader to read from the decrypted stream func (d *Decryptor) NewStreamReader() (io.Reader, error) { if d.key == nil { - return nil, fmt.Errorf("streamReader: wrapped-key not decrypted (missing SetPrivateKey()?") + return nil, ErrNoKey } if d.eof { @@ -158,7 +156,3 @@ func (r *encReader) Read(b []byte) (int, error) { return n, nil } - -var ( - errClosed = errors.New("encrypt: stream already closed") -) diff --git a/version b/version deleted file mode 100644 index 524cb55..0000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -1.1.1