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.
This commit is contained in:
Sudhi Herle 2021-05-15 19:35:54 -07:00
parent 3fa0ce0c9c
commit 945046a815
7 changed files with 325 additions and 269 deletions

View file

@ -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
@ -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:

37
build
View file

@ -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

View file

@ -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,12 +72,13 @@ 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
_SigtoolVersion = 2
_AEADNonceLen = 32
_FixedHdrLen = _MagicLen + 1 + 4
_WrapReceiverNonce = "Receiver Key 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)

43
sign/errors.go Normal file
View file

@ -0,0 +1,43 @@
// errors.go - list of all exportable errors in this module
//
// (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 (
"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")
)

View file

@ -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

View file

@ -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")
)

View file

@ -1 +0,0 @@
1.1.1