diff --git a/Makefile b/Makefile index 6c45e46..e46d43a 100644 --- a/Makefile +++ b/Makefile @@ -4,11 +4,10 @@ pwd = $(shell pwd) .PHONY: all test clean realclean all: - mkdir -p bin - go build -o bin/sigtool . + ./build -s test: go test ./sign clean realclean: - rm -f bin/sigtool + rm -rf bin diff --git a/README.md b/README.md index f8a0fa3..5921fef 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,54 @@ ## What is this? -`sigtool` is an opinionated tool to generate, sign and verify Ed25519 -signatures on files. In many ways, it is like like OpenBSD's signify_ --- except written in Golang and definitely easier to use. +`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. It can sign and verify very large files - it prehashes the files -with SHA-512 and then signs the SHA-512 checksum. +with SHA-512 and then signs the SHA-512 checksum. The keys and signatures +are YAML files and so, human readable. -All the artifacts produced by this tool are standard YAML files - -thus, human readable. +It can encrypt & decrypt files by converting the Ed25519 keys to their +corresponding Curve25519 variants. This elliptic co-ordinate transform +follows [FiloSottile's writeup][2]. The file encryption uses +AES-GCM-256 (AEAD); the input is broken into chunks and each chunk is +AEAD encrypted. The default chunk size is 4MB (4 * 1048576 bytes). + +A random 32-byte key is used to actually encrypt the file contents in +AES-GCM mode. This file-encryption key is **wrapped** using the recipient's +public key. Thus, a given input file (or stream) can be encrypted to be +read by multiple recipients - each of whom is identified by their Ed25519 +public keys. The file-encryptionb-key can optionally be wrapped using the +sender's Private Key - this authenticates the sender. If this private key is +not provided for the encrypt operation, then `sigtool` generates ephemeral +Curve25519 keys and wraps the file-encryption key using the ephemeral +private key and the recipient's public key. + +Every encrypted file starts with a header: + + 7 byte magic ("SigTool") + 1 byte version number + 4 byte header length + 32 byte SHA256 of the encryption-header + +The encryption-header is described as a protobuf file (sign/hdr.proto): + +```protobuf + message header { + uint32 chunk_size = 1; + bytes salt = 2; + repeated wrapped_key keys = 3; + } + + message wrapped_key { + bytes pk_hash = 1; // hash of Ed25519 PK + bytes pk = 2; // curve25519 PK + bytes nonce = 3; // AEAD nonce + bytes key = 4; // AEAD encrypted key + } +``` ## How do I build it? With Go 1.5 and later: @@ -21,7 +60,9 @@ With Go 1.5 and later: cd sigtool make -The binary will be in `./sigtool`. +The binary will be in `./bin/$HOSTOS-$ARCH/sigtool`. +where `$HOSTOS` is the host OS where you are building (e.g., openbsd) +and `$ARCH` is the CPU architecture (e.g., amd64). ## How do I use it? Broadly, the tool can: @@ -29,6 +70,8 @@ Broadly, the tool can: - generate new key pairs (public key and private key) - sign a file - verify a file against its signature +- encrypt a file +- decrypt a file ### Generate Key pair To start with, you generate a new key pair (a public key used for @@ -73,6 +116,22 @@ e.g., to verify the signature of *archive.tar.gz* against sigtool verify /tmp/testkey.pub archive.sig archive.tar.gz +### Encrypt a file by authenticating the sender +If the sender wishes to prove to the recipient that they encrypted +a file: + + sigtool encrypt -s mykey.key theirkey.pub -o archive.tar.gz.enc archive.tar.gz + + +This will create an encrypted file *archive.tar.gz.enc* such that the +recipient in possession of *theikey.key* can decrypt it. Furthermore, if +the recipient has *mykey.pub*, they can verify that the sender is indeed +who they expect. + +### Encrypt a file *without* authenticating the sender + +### Decrypt a file + ## 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 @@ -111,6 +170,8 @@ A serialized Ed25519 public key looks like so: ### Ed25519 Private Key And, a serialized Ed25519 private key looks like so: +```yaml + esk: t3vfqHbgUiA733KKPymFjWT8DdnBEkiMfsDHolPUdQWpvVn/F1Z4J6KYV3M5rGO9xgKxh5RAmqt+6LKgOiJAMQ== salt: pPHKG55UJYtJ5wU0G9hBvNQJ0DvT0a7T4Fmj4aPB84s= algo: scrypt-sha256 @@ -118,6 +179,7 @@ And, a serialized Ed25519 private key looks like so: 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 @@ -146,9 +208,12 @@ ensures that the supplied passphrase yields the same value as ### Ed25519 Signature A generated signature looks like below after serialization: +```yaml + comment: inpfile=/tmp/file.txt pkhash: 36z9tCwTIVNwwDlExrB0SQ== signature: ow2oBP+buDbEvlNakOrsxgB5Yc/7PYyPVZCkfyu7oahw8BakF4Qf32uswPaKGZ8RVz4uXboYHdZtfrEjCgP/Cg== +``` Here, ```pkhash`` is a SHA256 of the public key needed to verify this signature. @@ -163,4 +228,5 @@ See the file ``LICENSE.md`` for the full terms of the license. ## Author Sudhi Herle -.. _signify: https://www.openbsd.org/papers/bsdcan-signify.html +[1]: https://www.openbsd.org/papers/bsdcan-signify.html +[2]: https://blog.filippo.io/using-ed25519-keys-for-encryption/ diff --git a/build b/build new file mode 100755 index 0000000..8869d0a --- /dev/null +++ b/build @@ -0,0 +1,373 @@ +#! /usr/bin/env bash + +# Tool to build go programs in this repo +# +# - it tacks on a version number for use by the individual tools +# - it supports git and mercurial version# +# +# NB: +# o the attempt at decoding dirty repo state for mercurial is +# borked. It doesn't know about untracked files +# +# (c) 2016 Sudhi Herle +# +# License: GPLv2 +# +Progs=".:sigtool" + +# Relative path to protobuf sources +# e.g. src/foo/a.proto +Protobufs="sign/hdr.proto" + + +# -- DO NOT CHANGE ANYTHING AFTER THIS -- + +Z=`basename $0` +PWD=`pwd` + +Static=0 +Dryrun=0 +Prodver=0.1 +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 +} + +warn() { + echo "$Z: $@" 1>&2 +} + +case $BASH_VERSION in + 4.*|5.*) ;; + + *) die "I need bash 4.x to run!" + ;; +esac + + +# build a tool that runs on the host - if needed. +hosttool() { + local tool=$1 + local bindir=$2 + local src=$3 + + local p=$(type -P $tool) + if [ -n "$p" ]; then + echo $p + return 0 + fi + + # from here - we want this dir to find all build artifacts + PATH=$PATH:$bindir + export PATH + + p=$bindir/$tool + if [ -x $p ]; then + echo $p + return 0 + fi + + # build it and stash it in the hostdir + echo "Building tool $tool from $src .." + $e go get -d $src || exit 1 + $e go build -o $p $src || exit 1 + echo $p + return 0 +} + + +usage() { + + cat <&2 + fi +else + if [ $Static -gt 0 ]; then + export CGO_ENABLED=0 + + isuffix="--installsuffix cgo" + ldflags="-s" + msg="statically linked" + fi +fi + +# This is where build outputs go +Bindir=$PWD/bin/$cross +Hostbindir=$PWD/bin/$hostos-$hostcpu + +[ -d $Bindir ] || mkdir -p $Bindir +[ -d $Hostbindir ] || mkdir -p $Hostbindir + +# Get git/hg version info for the build +if [ -d "./.hg" ]; then + xrev=$(hg id --id) || exit 1 + brev=${xrev%+} + if [ "$brev" != "$xrev" ]; then + rev="hg:${brev}-dirty" + else + rev="hg:${brev}" + fi +elif [ -d "./.git" ]; then + xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1 + rev="git:$xrev" +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 "Need 'protoc' for building .." + + slick=$(hosttool protoc-gen-gogoslick $Hostbindir $slicksrc) || exit 1 + #if [ ! -f $slick ]; then + # echo "Building $slick .." + # $e go build -o $slick github.com/gogo/protobuf/protoc-gen-gogoslick || exit 1 + #i + + PATH=$Hostbindir:$PATH + export PATH + + 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 ..." + $e $pc --gogoslick_out=. $f || exit 1 + $e gofmt -w $of + fi + done +fi + +repover="main.RepoVersion=$rev" +prodver="main.ProductVersion=$Prodver" +date="main.Buildtime=`date -u '+%Y-%m-%dT%H:%M.%SZ'`" +ldflags="-ldflags \"-X $repover -X $prodver -X $date $ldflags\"" +vflag="" + +[ $Verbose -gt 0 ] && vflag="-v" + +case $Tool in + test) + set -- $args + $e go test $vflag "$@" + ;; + + vet) + set -- $args + $e go vet $vflag "$@" + ;; + + *) # Default is to build programs + set -- $args + if [ -z "$1" ]; then + all=$Progs + else + all="$@" + fi + + echo "Building $rev, $cross $msg .." + + for p in $all; do + if echo $p | grep -q ':' ; then + out=${p##*:} + dir=${p%%:*} + else + out=$p + dir=$p + fi + echo " $dir: $out .. " + $e eval go build $vflag -o $Bindir/$out $isuffix "$ldflags" ./$dir || exit 1 + done + ;; +esac + +# vim: ft=sh:expandtab:ts=4:sw=4:tw=84: diff --git a/crypt.go b/crypt.go index 3bd6846..a12612a 100644 --- a/crypt.go +++ b/crypt.go @@ -18,8 +18,8 @@ import ( "io" "os" - flag "github.com/opencoff/pflag" "github.com/opencoff/go-utils" + flag "github.com/opencoff/pflag" "github.com/opencoff/sigtool/sign" ) @@ -108,7 +108,10 @@ func encrypt(args []string) { outfd = outf } - var recip []*sign.PublicKey + en, err := sign.NewEncryptor(sk) + if err != nil { + die("%s", err) + } for i := 0; i < len(args)-1; i++ { fn := args[i] @@ -116,11 +119,18 @@ func encrypt(args []string) { if err != nil { die("%s", err) } - recip = append(recip, pk) + + err = en.AddRecipient(pk) + if err != nil { + die("%s", err) + } } - encryptFile(sk, recip, infd, outfd) + err = en.Encrypt(infd, outfd) + if err != nil { + die("%s", err) + } } // sigtool decrypt a.key [file] [-o output] @@ -132,11 +142,13 @@ func decrypt(args []string) { var envpw string var outfile string + var pubkey string var pw bool fs.StringVarP(&outfile, "outfile", "o", "", "Write the output to file `F`") fs.BoolVarP(&pw, "password", "p", false, "Ask for passphrase to decrypt the private key") fs.StringVarP(&envpw, "env-password", "", "", "Use passphrase from environment variable `E`") + fs.StringVarP(&pubkey, "verify-sender", "v", "", "Verify that the sender matches public key in `F`") err := fs.Parse(args) if err != nil { @@ -153,7 +165,6 @@ func decrypt(args []string) { var inf *os.File var pws, infile string - if len(envpw) > 0 { pws = os.Getenv(envpw) } else if pw { @@ -169,6 +180,15 @@ func decrypt(args []string) { die("%s", err) } + var pk *sign.PublicKey + + if len(pubkey) > 0 { + pk, err = sign.ReadPublicKey(pubkey) + if err != nil { + die("%s", err) + } + } + if len(args) > 1 { infile = args[1] if infile != "-" { @@ -200,14 +220,21 @@ func decrypt(args []string) { outfd = outf } - decryptFile(sk, infd, outfd) -} + d, err := sign.NewDecryptor(infd, pk) + if err != nil { + die("%s", err) + } -func encryptFile(sk *sign.PrivateKey, pks []*sign.PublicKey, infd io.Reader, outfd io.Writer) { -} + err = d.SetPrivateKey(sk) + if err != nil { + die("%s", err) + } -func decryptFile(sk *sign.PrivateKey, infd io.Reader, outfd io.Writer) { + err = d.Decrypt(outfd) + if err != nil { + die("%s", err) + } } diff --git a/go.mod b/go.mod index c43f3a4..ef96194 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/opencoff/sigtool go 1.13 require ( + github.com/gogo/protobuf v1.3.1 github.com/opencoff/go-utils v0.3.0 github.com/opencoff/pflag v0.3.3 golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc diff --git a/go.sum b/go.sum index 4c67c65..e318adb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/opencoff/go-utils v0.3.0 h1:/TQXjf50o3GSB9MItog5L8Gf4GWJ4B5+rmqjB4g2RZQ= github.com/opencoff/go-utils v0.3.0/go.mod h1:c+7QUAiCCHcNH6OGvsZ0fviG7cgse8Y3ucg+xy7sGXM= github.com/opencoff/pflag v0.3.3 h1:yohZkwYGPkB34WXvUQzU5GyLhImnjfePDARUaE8me3U= @@ -11,6 +15,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/sign/encrypt.go b/sign/encrypt.go index db80852..a5877c6 100644 --- a/sign/encrypt.go +++ b/sign/encrypt.go @@ -17,98 +17,427 @@ import ( "crypto/aes" "crypto/cipher" "crypto/ed25519" + "crypto/sha256" "crypto/sha512" - "encoding/hex" + "crypto/subtle" "fmt" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" "io" "math/big" - "strings" + "bytes" + "encoding/binary" ) -// A File-encryption-key wrapped by the Ed25519 public key of the recipient -type WrappedKey struct { - Key []byte // KEK - wrapped with the Curve25519 PK of recipient - Pk []byte // Curve25519 PK used to wrap - PkHash []byte // hash of the corresponding Ed25519 PK + +// Encryption chunk size = 4MB +const chunkSize int = 4 * 1048576 + +const _Magic = "SigTool" +const _MagicLen = len(_Magic) +const _AEADNonceLen = 32 + + +// Encryptor holds the encryption context +type Encryptor struct { + Header + key [32]byte // file encryption key + + ae cipher.AEAD + sender *PrivateKey + started bool + + buf []byte } -func hx(b []byte) string { - return hex.EncodeToString(b) -} +// Create a new Encryption context and use the optional private key 'sk' for +// 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) (*Encryptor, error) { -func unhx(s string) ([]byte, error) { - return hex.DecodeString(s) -} + e := &Encryptor{ + Header: Header{ + ChunkSize: uint32(chunkSize), + Salt: make([]byte, _AEADNonceLen), + }, -func (w *WrappedKey) ToString() string { - return fmt.Sprintf("(ed25519 to=%x, pk=%x, kek=%x)", - hx(w.PkHash), hx(w.Pk), hx(w.Key)) -} - -func parseErr(s string, v ...interface{}) error { - return fmt.Errorf(s, v...) -} - -// Given an marshalled stream of bytes, return the PubKey, encrypted key -func ParseWrappedKey(s string) (*WrappedKey, error) { - s = strings.TrimSpace(s) - if s[0] != '(' { - return nil, parseErr("missing '(' in wrapped key") + sender: sk, } - if s[len(s)-1] != ')' { - return nil, parseErr("missing ')' in wrapped key") + randread(e.key[:]) + randread(e.Salt) + + aes, err := aes.NewCipher(e.key[:]) + if err != nil { + return nil, fmt.Errorf("encrypt: %s", err) } - s = s[1 : len(s)-1] - v := strings.Fields(s) - if len(v) != 3 { - return nil, parseErr("Incorrect number of elements (exp 3, saw %d) in wrapped key", len(v)) + e.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen) + if err != nil { + return nil, fmt.Errorf("encrypt: %s", err) } - var w WrappedKey + e.buf = make([]byte, chunkSize + 4 + e.ae.Overhead()) + return e, nil +} - for _, z := range v { - kw := strings.Split(z, "=") - if len(kw) != 2 { - return nil, parseErr("malformed key=value pair (%s) in wrapped key", z) - } - var err error - switch strings.ToLower(kw[0]) { - case "to": - w.PkHash, err = unhx(kw[1]) +// 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") + } - case "pk": - w.Pk, err = unhx(kw[1]) + var w *WrappedKey + var err error - case "kek": - w.Key, err = unhx(kw[1]) + if e.sender != nil { + w, err = e.sender.WrapKey(pk, e.key[:]) + } else { + w, err = pk.WrapKeyEphemeral(e.key[:]) + } + if err != nil { + return err + } - default: - return nil, parseErr("unknown keyword %s in wrapped key", kw[0]) - } + e.Keys = append(e.Keys, w) + return nil +} + +// Begin the encryption process by writing the header +func (e *Encryptor) start(wr io.Writer) error { + msize := e.Size() + + // marshal the header and recipients + hdrlen := _MagicLen + 1 + 4 + sha256.Size + + buf := make([]byte, hdrlen + msize) + hdrbuf := buf[hdrlen:] + + copy(buf[:], []byte(_Magic)) + + buf[_MagicLen] = 1 // file version# + + // The fixed header is the magic _and _ the length of the variable segment. + // So, we capture the length of the variable portion first. + binary.BigEndian.PutUint32(buf[_MagicLen + 1:], uint32(sha256.Size + msize)) + + // Now marshal the variable portion + _, err := e.MarshalToSizedBuffer(hdrbuf) + if err != nil { + return fmt.Errorf("encrypt: can't marshal header: %s", err) + } + + // and calculate the header checksum + cksum := buf[_MagicLen + 1 + 4:] + h := sha256.New() + h.Write(hdrbuf) + h.Sum(cksum[:0]) + + // Finally write it out + err = fullwrite(buf, 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 nil, parseErr("can't parse value for %s in wrapped key", kw[0]) + 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 { + err := e.start(wr) + if err != nil { + return err } } - if len(w.PkHash) != 16 { - return nil, parseErr("invalid PkHash length (exp 16, saw %d) in wrapped key", len(w.PkHash)) + buf := make([]byte, e.ChunkSize) + i := 0 + + for { + n, err := io.ReadAtLeast(rd, buf, int(e.ChunkSize)) + if n == 0 { + return nil + } + if n > 0 { + err = e.encrypt(buf[:n], wr, i) + if err != nil { + return err + } + + i++ + continue + } + + if err != nil && err != io.EOF { + return fmt.Errorf("encrypt: I/O read error: %s", err) + } + } +} + +// 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 int) error { + var b [8]byte + var noncebuf [32]byte + + binary.BigEndian.PutUint32(b[:4], uint32(e.ae.Overhead() + len(buf))) + binary.BigEndian.PutUint32(b[4:], uint32(i)) + + h := sha256.New() + h.Write(e.Salt) + h.Write(b[:]) + nonce := h.Sum(noncebuf[:0]) + + copy(e.buf[:4], b[:4]) + cbuf := e.buf[4:] + c := e.ae.Seal(cbuf[:0], nonce, buf, b[:]) + + n := len(c) + 4 + + err := fullwrite(e.buf[:n], wr) + if err != nil { + return fmt.Errorf("encrypt: %s", err) + } + return nil +} + + +// Decryptor holds the decryption context +type Decryptor struct { + Header + + ae cipher.AEAD + rd io.Reader + buf []byte + + // Decrypted key + key []byte +} + + +// Create a new decryption context and if 'pk' is given, check that it matches +// the sender +func NewDecryptor(rd io.Reader, pk *PublicKey) (*Decryptor, error) { + var b [12]byte + + _, err := io.ReadFull(rd, b[:]) + if err != nil { + return nil, err } - if len(w.Pk) != 32 { - return nil, parseErr("invalid Public Key length (exp 32, saw %d) in wrapped key", len(w.Pk)) + if bytes.Compare(b[:_MagicLen], []byte(_Magic)) != 0 { + return nil, fmt.Errorf("decrypt: Not a sigtool encrypted file?") } - if len(w.Key) != 32 { - return nil, parseErr("invalid Key length (exp 32, saw %d) in wrapped key", len(w.Key)) + if b[_MagicLen] != 1 { + return nil, fmt.Errorf("decrypt: Unsupported version %d", b[_MagicLen]) } - return &w, nil + hdrlen := binary.BigEndian.Uint32(b[_MagicLen+1:]) + if hdrlen > 65536 { + return nil, fmt.Errorf("decrypt: header too large (max 65536)") + } + if hdrlen < 32 { + return nil, fmt.Errorf("decrypt: header too small (min 32)") + } + + hdr := make([]byte, hdrlen) + + _, err = io.ReadFull(rd, hdr) + if err != nil { + return nil, err + } + + verify := hdr[:32] + hdr = hdr[32:] + + cksum := sha256.Sum256(hdr) + if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 { + return nil, fmt.Errorf("decrypt: header corrupted") + } + + d := &Decryptor{ + rd: rd, + } + + err = d.Header.Unmarshal(hdr) + if err != nil { + return nil, fmt.Errorf("decrypt: decode error: %s", err) + } + + if d.ChunkSize == 0 || d.ChunkSize > (16 * 1048576) { + return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize) + } + + if len(d.Salt) != 32 { + return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt)) + } + + if len(d.Keys) == 0 { + return nil, fmt.Errorf("decrypt: no wrapped keys") + } + + // sanity check on the wrapped keys + for i, w := range d.Keys { + if len(w.PkHash) != PKHashLength { + return nil, fmt.Errorf("decrypt: wrapped key %d: invalid PkHash", i) + } + + if len(w.Pk) != 32 { + return nil, fmt.Errorf("decrypt: wrapped key %d: invalid Curve25519 PK", i) + } + + // XXX Default AES-256-GCM Nonce size is 12 + if len(w.Nonce) != 12 { + return nil, fmt.Errorf("decrypt: wrapped key %d: invalid Nonce", i) + } + + if len(w.Key) == 0 { + return nil, fmt.Errorf("decrypt: wrapped key %d: missing encrypted key", i) + } + + } + + d.buf = make([]byte, d.ChunkSize) + if pk != nil { + validSender := false + pkh := pk.Hash() + for _, w := range d.Keys { + if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { + validSender = true + } + } + + if !validSender { + return nil, fmt.Errorf("decrypt: Can't find sender's public key in the header") + } + } + + return d, nil +} + +// Use Private Key 'sk' to decrypt the encrypted keys in the header +func (d *Decryptor) SetPrivateKey(sk *PrivateKey) error { + var err error + + pkh := sk.PublicKey().Hash() + for i, w := range d.Keys { + if subtle.ConstantTimeCompare(pkh, w.PkHash) == 1 { + d.key, err = w.UnwrapKey(sk) + if err != nil { + return fmt.Errorf("decrypt: can't unwrap key %d: %s", i, err) + } + goto havekey + } + } + + return fmt.Errorf("decrypt: Can't find any public key to match the given private key") + + +havekey: + aes, err := aes.NewCipher(d.key) + if err != nil { + return fmt.Errorf("decrypt: %s", err) + } + + d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen) + if err != nil { + return fmt.Errorf("decrypt: %s", err) + } + return nil +} + +// Return a list of Wrapped keys in the encrypted file header +func (d *Decryptor) WrappedKeys() []*WrappedKey { + return d.Keys +} + + +// 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()?") + } + + for i := 0; ; i++ { + c, err := d.decrypt(i) + if err != nil { + return err + } + if len(c) == 0 { + return nil + } + + if len(c) > 0 { + err = fullwrite(c, wr) + if err != nil { + return fmt.Errorf("decrypt: %s", err) + } + } + } + return nil +} + +// Decrypt exactly one chunk of data +func (d *Decryptor) decrypt(i int) ([]byte, error) { + var b [8]byte + var nonceb [32]byte + + n, err := io.ReadFull(d.rd, b[:4]) + if n == 0 || err == io.EOF { + return nil, nil + } + + if err != nil { + return nil, fmt.Errorf("decrypt: can't read chunk %d length: %s", i, err) + } + + + chunklen := int(binary.BigEndian.Uint32(b[:4])) + binary.BigEndian.PutUint32(b[4:], uint32(i)) + h := sha256.New() + h.Write(d.Salt) + h.Write(b[:]) + nonce := h.Sum(nonceb[:0]) + + n, err = io.ReadFull(d.rd, d.buf[:chunklen]) + if n == 0 || err == io.EOF { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("decrypt: can't read chunk %d: %s", i, err) + } + + p, err := d.ae.Open(d.buf[:0], nonce, d.buf[:chunklen], b[:]) + if err != nil { + return nil, fmt.Errorf("decrypt: can't decrypt chunk %d: %s", i, err) + } + + return p, nil } // given a file-encryption-key, wrap it in the identity of the recipient 'pk' using our @@ -125,6 +454,23 @@ func (sk *PrivateKey) WrapKey(pk *PublicKey, key []byte) (*WrappedKey, error) { } +// Unwrap a wrapped key using the private key 'sk' +func (w *WrappedKey) UnwrapKey(sk *PrivateKey) ([]byte, error) { + var shared, theirPK, ourSK [32]byte + + pk := sk.PublicKey() + copy(ourSK[:], sk.toCurve25519SK()) + copy(theirPK[:], w.Pk) + curve25519.ScalarMult(&shared, &ourSK, &theirPK) + + key, err := aeadOpen(w.Key, w.Nonce, shared[:], pk.Pk) + if err != nil { + return nil, err + } + return key, nil +} + + // Wrap a shared key with the recipient's public key 'pk' by generating an ephemeral // Curve25519 keypair. This function does not identify the sender (non-repudiation). func (pk *PublicKey) WrapKeyEphemeral(key []byte) (*WrappedKey, error) { @@ -144,22 +490,16 @@ func (pk *PublicKey) WrapKeyEphemeral(key []byte) (*WrappedKey, error) { } func wrapKey(pk *PublicKey, k, theirPK, shared []byte) (*WrappedKey, error) { - - // hkdf or HMAC-sha-256 - kek, err := expand(shared[:], pk.Pk) - if err != nil { - return nil, fmt.Errorf("wrap: %s", err) - } - - ek, err := aeadSeal(k, kek) + ek, nonce, err := aeadSeal(k, shared[:], pk.Pk) if err != nil { return nil, fmt.Errorf("wrap: %s", err) } return &WrappedKey{ - Key: ek, - Pk: theirPK, PkHash: pk.hash, + Pk: theirPK, + Nonce: nonce, + Key: ek, }, nil } @@ -226,67 +566,65 @@ func expand(shared, pk []byte) ([]byte, error) { return kek, err } -func aeadSeal(data, key []byte) ([]byte, error) { - var salt [32]byte - var nonceb [64]byte - randread(salt[:]) - - h := sha512.New() - h.Write(salt[:]) - h.Write(key) - nonce := h.Sum(nonceb[:0])[:32] - - aes, err := aes.NewCipher(key) +// seal the data via AEAD after suitably expanding 'shared' +func aeadSeal(data, shared, pk []byte) ([]byte, []byte, error) { + kek, err := expand(shared[:], pk) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("wrap: %s", err) } - ae, err := cipher.NewGCMWithNonceSize(aes, len(nonce)) + aes, err := aes.NewCipher(kek) if err != nil { - return nil, err + return nil, nil, fmt.Errorf("wrap: %s", err) + } + + ae, err := cipher.NewGCM(aes) + if err != nil { + return nil, nil, fmt.Errorf("wrap: %s", err) + } + + noncesize := ae.NonceSize() + tagsize := ae.Overhead() + + buf := make([]byte, tagsize + len(kek)) + nonce := make([]byte, noncesize) + + randread(nonce) + + out := ae.Seal(buf[:0], nonce, data, nil) + return out, nonce, nil +} + +func aeadOpen(data, nonce, shared, pk []byte) ([]byte, error) { + // hkdf or HMAC-sha-256 + kek, err := expand(shared, pk) + if err != nil { + return nil, fmt.Errorf("unwrap: %s", err) + } + aes, err := aes.NewCipher(kek) + 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) + } + + want := 32 + ae.Overhead() + if len(data) != want { + return nil, fmt.Errorf("unwrap: incorrect decrypt bytes (need %d, saw %d)", want, len(data)) + } + + c, err := ae.Open(data[:0], nonce, data, nil) + if err != nil { + return nil, fmt.Errorf("unwrap: %s", err) } - c := ae.Seal(nil, nonce, data, nil) - c = append(c, salt[:]...) return c, nil } -func aeadOpen(data, key []byte) ([]byte, error) { - var nonceb [64]byte - // GCM tag: 16 bytes - // salt: 32 bytes - // last 32 bytes: salt - - n := len(data) - if n < (32 + 16) { - return nil, fmt.Errorf("aead: too few decrypt bytes (min 48, saw %d)", n) - } - - salt := data[n-32:] - data = data[:n-32] - - h := sha512.New() - h.Write(salt) - h.Write(key) - nonce := h.Sum(nonceb[:0])[:32] - - aes, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - - ae, err := cipher.NewGCMWithNonceSize(aes, len(nonce)) - if err != nil { - return nil, err - } - - c, err := ae.Open(nil, nonce, data, nil) - if err != nil { - return nil, err - } - return c, nil -} func clamp(k []byte) []byte { k[0] &= 248 diff --git a/sign/hdr.pb.go b/sign/hdr.pb.go new file mode 100644 index 0000000..eb45149 --- /dev/null +++ b/sign/hdr.pb.go @@ -0,0 +1,899 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: sign/hdr.proto + +package sign + +import ( + bytes "bytes" + fmt "fmt" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" + reflect "reflect" + strings "strings" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type Header struct { + ChunkSize uint32 `protobuf:"varint,1,opt,name=chunk_size,json=chunkSize,proto3" json:"chunk_size,omitempty"` + Salt []byte `protobuf:"bytes,2,opt,name=salt,proto3" json:"salt,omitempty"` + Keys []*WrappedKey `protobuf:"bytes,3,rep,name=keys,proto3" json:"keys,omitempty"` +} + +func (m *Header) Reset() { *m = Header{} } +func (*Header) ProtoMessage() {} +func (*Header) Descriptor() ([]byte, []int) { + return fileDescriptor_85aff542c746609f, []int{0} +} +func (m *Header) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Header) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Header.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Header) XXX_Merge(src proto.Message) { + xxx_messageInfo_Header.Merge(m, src) +} +func (m *Header) XXX_Size() int { + return m.Size() +} +func (m *Header) XXX_DiscardUnknown() { + xxx_messageInfo_Header.DiscardUnknown(m) +} + +var xxx_messageInfo_Header proto.InternalMessageInfo + +func (m *Header) GetChunkSize() uint32 { + if m != nil { + return m.ChunkSize + } + return 0 +} + +func (m *Header) GetSalt() []byte { + if m != nil { + return m.Salt + } + return nil +} + +func (m *Header) GetKeys() []*WrappedKey { + if m != nil { + return m.Keys + } + return nil +} + +type WrappedKey struct { + PkHash []byte `protobuf:"bytes,1,opt,name=pk_hash,json=pkHash,proto3" json:"pk_hash,omitempty"` + Pk []byte `protobuf:"bytes,2,opt,name=pk,proto3" json:"pk,omitempty"` + Nonce []byte `protobuf:"bytes,3,opt,name=nonce,proto3" json:"nonce,omitempty"` + Key []byte `protobuf:"bytes,4,opt,name=key,proto3" json:"key,omitempty"` +} + +func (m *WrappedKey) Reset() { *m = WrappedKey{} } +func (*WrappedKey) ProtoMessage() {} +func (*WrappedKey) Descriptor() ([]byte, []int) { + return fileDescriptor_85aff542c746609f, []int{1} +} +func (m *WrappedKey) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *WrappedKey) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_WrappedKey.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *WrappedKey) XXX_Merge(src proto.Message) { + xxx_messageInfo_WrappedKey.Merge(m, src) +} +func (m *WrappedKey) XXX_Size() int { + return m.Size() +} +func (m *WrappedKey) XXX_DiscardUnknown() { + xxx_messageInfo_WrappedKey.DiscardUnknown(m) +} + +var xxx_messageInfo_WrappedKey proto.InternalMessageInfo + +func (m *WrappedKey) GetPkHash() []byte { + if m != nil { + return m.PkHash + } + return nil +} + +func (m *WrappedKey) GetPk() []byte { + if m != nil { + return m.Pk + } + return nil +} + +func (m *WrappedKey) GetNonce() []byte { + if m != nil { + return m.Nonce + } + return nil +} + +func (m *WrappedKey) GetKey() []byte { + if m != nil { + return m.Key + } + return nil +} + +func init() { + proto.RegisterType((*Header)(nil), "sign.header") + proto.RegisterType((*WrappedKey)(nil), "sign.wrapped_key") +} + +func init() { proto.RegisterFile("sign/hdr.proto", fileDescriptor_85aff542c746609f) } + +var fileDescriptor_85aff542c746609f = []byte{ + // 257 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x90, 0x31, 0x4a, 0xc4, 0x40, + 0x14, 0x86, 0xe7, 0x25, 0x31, 0xe2, 0xdb, 0x75, 0xd1, 0x41, 0x30, 0x8d, 0x8f, 0xb0, 0x20, 0xa4, + 0x8a, 0xa0, 0x9e, 0xc0, 0xca, 0x3a, 0xf6, 0x86, 0xec, 0x66, 0x70, 0xc2, 0x48, 0x32, 0x64, 0x56, + 0x24, 0x5b, 0x79, 0x04, 0x8f, 0xe1, 0x51, 0x2c, 0x53, 0x6e, 0x69, 0x26, 0x8d, 0xe5, 0x1e, 0x41, + 0x32, 0x5a, 0xd8, 0xfd, 0xff, 0xf7, 0xe0, 0x7d, 0xf0, 0xe3, 0xc2, 0x54, 0x4f, 0xf5, 0x95, 0x2c, + 0xdb, 0x54, 0xb7, 0xcd, 0xa6, 0xe1, 0xc1, 0xd4, 0x97, 0x2b, 0x0c, 0xa5, 0x28, 0x4a, 0xd1, 0xf2, + 0x0b, 0xc4, 0xb5, 0x7c, 0xa9, 0x55, 0x6e, 0xaa, 0xad, 0x88, 0x20, 0x86, 0xe4, 0x38, 0x3b, 0x72, + 0xe4, 0xa1, 0xda, 0x0a, 0xce, 0x31, 0x30, 0xc5, 0xf3, 0x26, 0xf2, 0x62, 0x48, 0xe6, 0x99, 0xcb, + 0xfc, 0x12, 0x03, 0x25, 0x3a, 0x13, 0xf9, 0xb1, 0x9f, 0xcc, 0xae, 0x4f, 0xd3, 0xe9, 0x63, 0xfa, + 0xda, 0x16, 0x5a, 0x8b, 0x32, 0x57, 0xa2, 0xcb, 0xdc, 0x79, 0xf9, 0x88, 0xb3, 0x7f, 0x90, 0x9f, + 0xe3, 0xa1, 0x56, 0xb9, 0x2c, 0x8c, 0x74, 0x96, 0x79, 0x16, 0x6a, 0x75, 0x5f, 0x18, 0xc9, 0x17, + 0xe8, 0x69, 0xf5, 0x27, 0xf0, 0xb4, 0xe2, 0x67, 0x78, 0x50, 0x37, 0xf5, 0x5a, 0x44, 0xbe, 0x43, + 0xbf, 0x85, 0x9f, 0xa0, 0xaf, 0x44, 0x17, 0x05, 0x8e, 0x4d, 0xf1, 0xee, 0xb6, 0x1f, 0x88, 0xed, + 0x06, 0x62, 0xfb, 0x81, 0xe0, 0xcd, 0x12, 0x7c, 0x58, 0x82, 0x4f, 0x4b, 0xd0, 0x5b, 0x82, 0x2f, + 0x4b, 0xf0, 0x6d, 0x89, 0xed, 0x2d, 0xc1, 0xfb, 0x48, 0xac, 0x1f, 0x89, 0xed, 0x46, 0x62, 0xab, + 0xd0, 0xcd, 0x70, 0xf3, 0x13, 0x00, 0x00, 0xff, 0xff, 0xde, 0xf2, 0x28, 0xc0, 0x18, 0x01, 0x00, + 0x00, +} + +func (this *Header) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Header) + if !ok { + that2, ok := that.(Header) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.ChunkSize != that1.ChunkSize { + return false + } + if !bytes.Equal(this.Salt, that1.Salt) { + return false + } + if len(this.Keys) != len(that1.Keys) { + return false + } + for i := range this.Keys { + if !this.Keys[i].Equal(that1.Keys[i]) { + return false + } + } + return true +} +func (this *WrappedKey) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*WrappedKey) + if !ok { + that2, ok := that.(WrappedKey) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if !bytes.Equal(this.PkHash, that1.PkHash) { + return false + } + if !bytes.Equal(this.Pk, that1.Pk) { + return false + } + if !bytes.Equal(this.Nonce, that1.Nonce) { + return false + } + if !bytes.Equal(this.Key, that1.Key) { + return false + } + return true +} +func (this *Header) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 7) + s = append(s, "&sign.Header{") + s = append(s, "ChunkSize: "+fmt.Sprintf("%#v", this.ChunkSize)+",\n") + s = append(s, "Salt: "+fmt.Sprintf("%#v", this.Salt)+",\n") + if this.Keys != nil { + s = append(s, "Keys: "+fmt.Sprintf("%#v", this.Keys)+",\n") + } + s = append(s, "}") + return strings.Join(s, "") +} +func (this *WrappedKey) GoString() string { + if this == nil { + return "nil" + } + s := make([]string, 0, 8) + s = append(s, "&sign.WrappedKey{") + s = append(s, "PkHash: "+fmt.Sprintf("%#v", this.PkHash)+",\n") + s = append(s, "Pk: "+fmt.Sprintf("%#v", this.Pk)+",\n") + s = append(s, "Nonce: "+fmt.Sprintf("%#v", this.Nonce)+",\n") + s = append(s, "Key: "+fmt.Sprintf("%#v", this.Key)+",\n") + s = append(s, "}") + return strings.Join(s, "") +} +func valueToGoStringHdr(v interface{}, typ string) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("func(v %v) *%v { return &v } ( %#v )", typ, typ, pv) +} +func (m *Header) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Header) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Header) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Keys) > 0 { + for iNdEx := len(m.Keys) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Keys[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintHdr(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Salt) > 0 { + i -= len(m.Salt) + copy(dAtA[i:], m.Salt) + i = encodeVarintHdr(dAtA, i, uint64(len(m.Salt))) + i-- + dAtA[i] = 0x12 + } + if m.ChunkSize != 0 { + i = encodeVarintHdr(dAtA, i, uint64(m.ChunkSize)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *WrappedKey) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *WrappedKey) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *WrappedKey) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Key) > 0 { + i -= len(m.Key) + copy(dAtA[i:], m.Key) + i = encodeVarintHdr(dAtA, i, uint64(len(m.Key))) + i-- + dAtA[i] = 0x22 + } + if len(m.Nonce) > 0 { + i -= len(m.Nonce) + copy(dAtA[i:], m.Nonce) + i = encodeVarintHdr(dAtA, i, uint64(len(m.Nonce))) + i-- + dAtA[i] = 0x1a + } + if len(m.Pk) > 0 { + i -= len(m.Pk) + copy(dAtA[i:], m.Pk) + i = encodeVarintHdr(dAtA, i, uint64(len(m.Pk))) + i-- + dAtA[i] = 0x12 + } + if len(m.PkHash) > 0 { + i -= len(m.PkHash) + copy(dAtA[i:], m.PkHash) + i = encodeVarintHdr(dAtA, i, uint64(len(m.PkHash))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintHdr(dAtA []byte, offset int, v uint64) int { + offset -= sovHdr(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *Header) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.ChunkSize != 0 { + n += 1 + sovHdr(uint64(m.ChunkSize)) + } + l = len(m.Salt) + if l > 0 { + n += 1 + l + sovHdr(uint64(l)) + } + if len(m.Keys) > 0 { + for _, e := range m.Keys { + l = e.Size() + n += 1 + l + sovHdr(uint64(l)) + } + } + return n +} + +func (m *WrappedKey) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.PkHash) + if l > 0 { + n += 1 + l + sovHdr(uint64(l)) + } + l = len(m.Pk) + if l > 0 { + n += 1 + l + sovHdr(uint64(l)) + } + l = len(m.Nonce) + if l > 0 { + n += 1 + l + sovHdr(uint64(l)) + } + l = len(m.Key) + if l > 0 { + n += 1 + l + sovHdr(uint64(l)) + } + return n +} + +func sovHdr(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozHdr(x uint64) (n int) { + return sovHdr(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (this *Header) String() string { + if this == nil { + return "nil" + } + repeatedStringForKeys := "[]*WrappedKey{" + for _, f := range this.Keys { + repeatedStringForKeys += strings.Replace(fmt.Sprintf("%v", f), "WrappedKey", "WrappedKey", 1) + "," + } + repeatedStringForKeys += "}" + s := strings.Join([]string{`&Header{`, + `ChunkSize:` + fmt.Sprintf("%v", this.ChunkSize) + `,`, + `Salt:` + fmt.Sprintf("%v", this.Salt) + `,`, + `Keys:` + repeatedStringForKeys + `,`, + `}`, + }, "") + return s +} +func (this *WrappedKey) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&WrappedKey{`, + `PkHash:` + fmt.Sprintf("%v", this.PkHash) + `,`, + `Pk:` + fmt.Sprintf("%v", this.Pk) + `,`, + `Nonce:` + fmt.Sprintf("%v", this.Nonce) + `,`, + `Key:` + fmt.Sprintf("%v", this.Key) + `,`, + `}`, + }, "") + return s +} +func valueToStringHdr(v interface{}) string { + rv := reflect.ValueOf(v) + if rv.IsNil() { + return "nil" + } + pv := reflect.Indirect(rv).Interface() + return fmt.Sprintf("*%v", pv) +} +func (m *Header) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: header: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: header: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ChunkSize", wireType) + } + m.ChunkSize = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ChunkSize |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Salt", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Salt = append(m.Salt[:0], dAtA[iNdEx:postIndex]...) + if m.Salt == nil { + m.Salt = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Keys", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Keys = append(m.Keys, &WrappedKey{}) + if err := m.Keys[len(m.Keys)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipHdr(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthHdr + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthHdr + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *WrappedKey) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: wrapped_key: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: wrapped_key: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PkHash", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PkHash = append(m.PkHash[:0], dAtA[iNdEx:postIndex]...) + if m.PkHash == nil { + m.PkHash = []byte{} + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pk", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Pk = append(m.Pk[:0], dAtA[iNdEx:postIndex]...) + if m.Pk == nil { + m.Pk = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Nonce", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Nonce = append(m.Nonce[:0], dAtA[iNdEx:postIndex]...) + if m.Nonce == nil { + m.Nonce = []byte{} + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowHdr + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthHdr + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthHdr + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...) + if m.Key == nil { + m.Key = []byte{} + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipHdr(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthHdr + } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthHdr + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipHdr(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowHdr + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowHdr + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowHdr + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthHdr + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupHdr + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthHdr + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthHdr = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowHdr = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupHdr = fmt.Errorf("proto: unexpected end of group") +) diff --git a/sign/hdr.proto b/sign/hdr.proto new file mode 100644 index 0000000..509312a --- /dev/null +++ b/sign/hdr.proto @@ -0,0 +1,23 @@ +syntax="proto3"; + +//import "gogoproto/gogo.proto" + +package sign; + +//option (gogoproto.marshaler_all) = true; +//option (gogoproto.sizer_all) = true; +//option (gogoproto.unmarshaler_all) = true; +//option (gogoproto.goproto_getters_all) = false; + +message header { + uint32 chunk_size = 1; + bytes salt = 2; + repeated wrapped_key keys = 3; +} + +message wrapped_key { + bytes pk_hash = 1; // hash of Ed25519 PK + bytes pk = 2; // curve25519 PK + bytes nonce = 3; // AEAD nonce + bytes key = 4; // AEAD encrypted key +} diff --git a/sign/sign.go b/sign/sign.go index 0e8fab3..87d08ef 100644 --- a/sign/sign.go +++ b/sign/sign.go @@ -77,6 +77,9 @@ type Signature struct { const sk_algo = "scrypt-sha256" const sig_algo = "sha512-ed25519" +// Length of Ed25519 Public Key Hash +const PKHashLength = 16 + // Scrypt parameters const _N = 1 << 17 const _r = 16 @@ -132,7 +135,7 @@ type signature struct { func pkhash(pk []byte) []byte { z := sha256.Sum256(pk) - return z[:16] + return z[:PKHashLength] } // Generate a new Ed25519 keypair @@ -220,7 +223,6 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) { return nil, fmt.Errorf("can't decode YAML:Verify: %s", err) } - sk := &PrivateKey{} // We take short passwords and extend them pwb := sha512.Sum512([]byte(pw)) @@ -240,11 +242,24 @@ func MakePrivateKey(yml []byte, pw string) (*PrivateKey, error) { } // Everything works. Now, decode the key - sk.Sk = make([]byte, len(esk.Esk)) + skb := make([]byte, len(esk.Esk)) for i := 0; i < len(esk.Esk); i++ { - sk.Sk[i] = esk.Esk[i] ^ xork[i] + skb[i] = esk.Esk[i] ^ xork[i] } + edsk := Ed.PrivateKey(skb) + edpk := edsk.Public().(Ed.PublicKey) + + pk := &PublicKey{ + Pk: []byte(edpk), + hash: pkhash([]byte(edpk)), + } + sk := &PrivateKey{ + Sk: skb, + pk: pk, + } + + return sk, nil } diff --git a/sigtool.go b/sigtool.go index 2ee2319..c21013b 100644 --- a/sigtool.go +++ b/sigtool.go @@ -306,6 +306,8 @@ Commands: generate, g Generate a new Ed25519 keypair sign, s Sign a file with a private key verify, v Verify a signature against a file and a public key + encrypt, e Encrypt an input file to one or more recipients + decrypt, d Decrypt a file with a private key `, Z, Z) os.Stdout.Write([]byte(x)) diff --git a/version b/version new file mode 100644 index 0000000..0ea3a94 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.2.0