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:
parent
3fa0ce0c9c
commit
945046a815
7 changed files with 325 additions and 269 deletions
72
README.md
72
README.md
|
@ -7,26 +7,26 @@
|
||||||
`sigtool` is an opinionated tool to generate keys, sign, verify, encrypt &
|
`sigtool` is an opinionated tool to generate keys, sign, verify, encrypt &
|
||||||
decrypt files using Ed25519 signature scheme. In many ways, it is like
|
decrypt files using Ed25519 signature scheme. In many ways, it is like
|
||||||
like OpenBSD's [signify][1] -- except written in Golang and definitely
|
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
|
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
|
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
|
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
|
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
|
during the encryption process - this has the benefit of also authenticating
|
||||||
the sender (and the receiver can verify the sender if they possess the
|
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
|
The sign, verify, encrypt, decrypt operations can use OpenSSH Ed25519 keys
|
||||||
*or* the keys generated by sigtool. This means, you can send encrypted
|
*or* the keys generated by sigtool. This means, you can send encrypted
|
||||||
files to any recipient identified by their comment in `~/.ssh/authorized_keys`.
|
files to any recipient identified by their comment in `~/.ssh/authorized_keys`.
|
||||||
|
|
||||||
## How do I build it?
|
## How do I build it?
|
||||||
With Go 1.5 and later:
|
With Go 1.13 and later:
|
||||||
|
|
||||||
git clone https://github.com/opencoff/sigtool
|
git clone https://github.com/opencoff/sigtool
|
||||||
cd 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.
|
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 input is broken into chunks and each chunk is individually AEAD encrypted.
|
||||||
The default chunk size is 4MB (4 * 1048576 bytes). Each chunk generates
|
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
|
its own nonce from a global salt. The nonce is calculated as follows:
|
||||||
the salt, the chunk length and the block number.
|
|
||||||
|
|
||||||
### 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
|
`sigtool` uses ephemeral Curve25519 keys to generate shared secrets
|
||||||
between pairs of sender & one or more recipients. This pairwise shared
|
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
|
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
|
If the sender authenticates the encryption by providing their secret
|
||||||
key, the data-encryption key is signed via Ed25519 and the signature
|
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.
|
zeroes is encrypted instead.
|
||||||
|
|
||||||
The Ed25519 keys generated by `sigtool` or OpenSSH are transformed to their
|
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].
|
This elliptic co-ordinate transform follows [FiloSottile's writeup][2].
|
||||||
|
|
||||||
### Format of the Encrypted File
|
### Format of the Encrypted File
|
||||||
|
@ -205,19 +211,33 @@ chunk is encoded the same way:
|
||||||
|
|
||||||
```C
|
```C
|
||||||
4 byte chunk length (big endian encoding)
|
4 byte chunk length (big endian encoding)
|
||||||
encrypted chunk data
|
AEAD encrypted chunk data
|
||||||
AEAD tag
|
AEAD tag
|
||||||
```
|
```
|
||||||
|
|
||||||
The chunk length does _not_ include the AEAD tag length; it is implicitly
|
The chunk length does _not_ include the AEAD tag length; it is implicitly
|
||||||
computed.
|
computed. The chunk data and AEAD tag are treated as an atomic unit for AEAD
|
||||||
|
|
||||||
The chunk data and AEAD tag are treated as an atomic unit for AEAD
|
|
||||||
decryption.
|
decryption.
|
||||||
|
|
||||||
### How is the private key protected?
|
### How is the private key protected?
|
||||||
The Ed25519 private key is encrypted in AES-GCM-256 mode using a key
|
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
|
## Understanding the Code
|
||||||
|
@ -263,24 +283,6 @@ And, a serialized Ed25519 private key looks like so:
|
||||||
p: 1
|
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
|
### Ed25519 Signature
|
||||||
A generated signature looks like below after serialization:
|
A generated signature looks like below after serialization:
|
||||||
|
|
37
build
37
build
|
@ -27,14 +27,12 @@ PWD=`pwd`
|
||||||
|
|
||||||
Static=0
|
Static=0
|
||||||
Dryrun=0
|
Dryrun=0
|
||||||
Prodver=0.1
|
Prodver=""
|
||||||
Verbose=0
|
Verbose=0
|
||||||
|
|
||||||
hostos=$(go env GOHOSTOS) || exit 1
|
hostos=$(go env GOHOSTOS) || exit 1
|
||||||
hostcpu=$(go env GOHOSTARCH) || exit 1
|
hostcpu=$(go env GOHOSTARCH) || exit 1
|
||||||
|
|
||||||
[ -f ./version ] && Prodver=$(cat ./version)
|
|
||||||
|
|
||||||
die() {
|
die() {
|
||||||
echo "$Z: $@" 1>&2
|
echo "$Z: $@" 1>&2
|
||||||
exit 0
|
exit 0
|
||||||
|
@ -102,7 +100,8 @@ And, PROGS is one or more go programs.
|
||||||
|
|
||||||
With no arguments, $0 builds: $Progs (source in ./src/)
|
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:
|
Options:
|
||||||
-h, --help Show this help message and quit
|
-h, --help Show this help message and quit
|
||||||
|
@ -153,6 +152,7 @@ done
|
||||||
Tool=
|
Tool=
|
||||||
doinit=0
|
doinit=0
|
||||||
args=
|
args=
|
||||||
|
Printarch=0
|
||||||
|
|
||||||
#set -x
|
#set -x
|
||||||
ac_prev=
|
ac_prev=
|
||||||
|
@ -214,6 +214,10 @@ do
|
||||||
set -x
|
set -x
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
--print-arch)
|
||||||
|
Printarch=1
|
||||||
|
;;
|
||||||
|
|
||||||
*) # first non option terminates option processing.
|
*) # first non option terminates option processing.
|
||||||
# we gather all remaining args and bundle them up.
|
# we gather all remaining args and bundle them up.
|
||||||
args="$args $ac_option"
|
args="$args $ac_option"
|
||||||
|
@ -228,7 +232,7 @@ done
|
||||||
[ $Dryrun -gt 0 ] && e=echo
|
[ $Dryrun -gt 0 ] && e=echo
|
||||||
|
|
||||||
# let every error abort
|
# let every error abort
|
||||||
#set -e
|
set -e
|
||||||
|
|
||||||
# This fragment can't be in a function - since it exports several vars
|
# This fragment can't be in a function - since it exports several vars
|
||||||
if [ -n "$Arch" ]; then
|
if [ -n "$Arch" ]; then
|
||||||
|
@ -274,6 +278,12 @@ else
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ $Printarch -gt 0 ]; then
|
||||||
|
echo "$hostos-$hostcpu"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# This is where build outputs go
|
# This is where build outputs go
|
||||||
Bindir=$PWD/bin/$cross
|
Bindir=$PWD/bin/$cross
|
||||||
Hostbindir=$PWD/bin/$hostos-$hostcpu
|
Hostbindir=$PWD/bin/$hostos-$hostcpu
|
||||||
|
@ -290,21 +300,28 @@ if [ -d "./.hg" ]; then
|
||||||
else
|
else
|
||||||
rev="hg:${brev}"
|
rev="hg:${brev}"
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$Prodver" ]; then
|
||||||
|
Prodver=$(hg log -r "branch(stable) and tag()" -T "{tags}\n" | tail -1)
|
||||||
|
fi
|
||||||
elif [ -d "./.git" ]; then
|
elif [ -d "./.git" ]; then
|
||||||
xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1
|
xrev=$(git describe --always --dirty --long --abbrev=12) || exit 1
|
||||||
rev="git:$xrev"
|
rev="git:$xrev"
|
||||||
|
if [ -z "$Prodver" ]; then
|
||||||
|
Prodver=$(git tag --list | tail -1)
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
rev="UNKNOWN-VER"
|
rev="UNKNOWN-VER"
|
||||||
echo "$0: Can't find version info" 1>&2
|
echo "$0: Can't find version info" 1>&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Do Protobufs if needed
|
# Do Protobufs if needed
|
||||||
if [ -n "$Protobufs" ]; then
|
if [ -n "$Protobufs" ]; then
|
||||||
slick=$Hostbindir/protoc-gen-gogoslick
|
slick=$Hostbindir/protoc-gen-gogoslick
|
||||||
slicksrc=github.com/gogo/protobuf/protoc-gen-gogoslick
|
slicksrc=github.com/gogo/protobuf/protoc-gen-gogoslick
|
||||||
pc=$(type -p protoc)
|
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
|
slick=$(hosttool protoc-gen-gogoslick $Hostbindir $slicksrc) || exit 1
|
||||||
#if [ ! -f $slick ]; then
|
#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
|
# $e go build -o $slick github.com/gogo/protobuf/protoc-gen-gogoslick || exit 1
|
||||||
#i
|
#i
|
||||||
|
|
||||||
PATH=$Hostbindir:$PATH
|
export PATH=$PATH:$Hostbindir
|
||||||
export PATH
|
|
||||||
|
|
||||||
for f in $Protobufs; do
|
for f in $Protobufs; do
|
||||||
dn=$(dirname $f)
|
dn=$(dirname $f)
|
||||||
bn=$(basename $f .proto)
|
bn=$(basename $f .proto)
|
||||||
of=$dn/${bn}.pb.go
|
of=$dn/${bn}.pb.go
|
||||||
if [ $f -nt $of ]; then
|
if [ $f -nt $of ]; then
|
||||||
echo "gogoslick: $f -> $of ..."
|
echo "Running $pc .."
|
||||||
$e $pc --gogoslick_out=. $f || exit 1
|
$e $pc --gogoslick_out=. $f || exit 1
|
||||||
$e gofmt -w $of
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
@ -354,7 +369,7 @@ case $Tool in
|
||||||
all="$@"
|
all="$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Building $msg $Prodver ($rev) for $cross .."
|
echo "Building $Prodver ($rev), $cross $msg .."
|
||||||
|
|
||||||
for p in $all; do
|
for p in $all; do
|
||||||
if echo $p | grep -q ':' ; then
|
if echo $p | grep -q ':' ; then
|
||||||
|
|
411
sign/encrypt.go
411
sign/encrypt.go
|
@ -42,7 +42,7 @@
|
||||||
// The input data is broken up into "chunks"; each no larger than
|
// The input data is broken up into "chunks"; each no larger than
|
||||||
// maxChunkSize. The default block size is "chunkSize". Each block
|
// maxChunkSize. The default block size is "chunkSize". Each block
|
||||||
// is AEAD encrypted:
|
// 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
|
// The encrypted block (includes the AEAD tag) length is written
|
||||||
// as a big-endian 4-byte prefix. The high-order bit of this length
|
// as a big-endian 4-byte prefix. The high-order bit of this length
|
||||||
|
@ -72,12 +72,13 @@ import (
|
||||||
// Encryption chunk size = 4MB
|
// Encryption chunk size = 4MB
|
||||||
const (
|
const (
|
||||||
chunkSize uint32 = 4 * 1048576
|
chunkSize uint32 = 4 * 1048576
|
||||||
maxChunkSize uint32 = 16 * 1048576
|
maxChunkSize uint32 = 1 << 30
|
||||||
_EOF uint32 = 1 << 31
|
_EOF uint32 = 1 << 31
|
||||||
|
|
||||||
_Magic = "SigTool"
|
_Magic = "SigTool"
|
||||||
_MagicLen = len(_Magic)
|
_MagicLen = len(_Magic)
|
||||||
_AEADNonceLen = 16
|
_SigtoolVersion = 2
|
||||||
|
_AEADNonceLen = 32
|
||||||
_FixedHdrLen = _MagicLen + 1 + 4
|
_FixedHdrLen = _MagicLen + 1 + 4
|
||||||
|
|
||||||
_WrapReceiverNonce = "Receiver Key Nonce"
|
_WrapReceiverNonce = "Receiver Key Nonce"
|
||||||
|
@ -119,7 +120,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||||
// generate ephemeral Curve25519 keys
|
// generate ephemeral Curve25519 keys
|
||||||
esk, epk, err := newSender()
|
esk, epk, err := newSender()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("encrypt: %s", err)
|
return nil, fmt.Errorf("encrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
key := make([]byte, 32)
|
key := make([]byte, 32)
|
||||||
|
@ -134,7 +135,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||||
if sk != nil {
|
if sk != nil {
|
||||||
sig, err := sk.SignMessage(key, "")
|
sig, err := sk.SignMessage(key, "")
|
||||||
if err != nil {
|
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
|
senderSig = sig.Sig
|
||||||
|
@ -145,7 +146,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||||
|
|
||||||
wSig, err := wrapSenderSig(senderSig, key, salt)
|
wSig, err := wrapSenderSig(senderSig, key, salt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("encrypt: %s", err)
|
return nil, fmt.Errorf("encrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e := &Encryptor{
|
e := &Encryptor{
|
||||||
|
@ -166,7 +167,7 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
|
||||||
// Add a new recipient to this encryption context.
|
// Add a new recipient to this encryption context.
|
||||||
func (e *Encryptor) AddRecipient(pk *PublicKey) error {
|
func (e *Encryptor) AddRecipient(pk *PublicKey) error {
|
||||||
if e.started {
|
if e.started {
|
||||||
return fmt.Errorf("encrypt: can't add new recipient after encryption has started")
|
return ErrEncStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := e.wrapKey(pk)
|
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'
|
// Encrypt the input stream 'rd' and write encrypted stream to 'wr'
|
||||||
func (e *Encryptor) Encrypt(rd io.Reader, wr io.WriteCloser) error {
|
func (e *Encryptor) Encrypt(rd io.Reader, wr io.WriteCloser) error {
|
||||||
if e.stream {
|
if e.stream {
|
||||||
return fmt.Errorf("encrypt: can't use Encrypt() after using streaming I/O")
|
return ErrEncIsStream
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.started {
|
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:
|
case io.EOF, io.ErrClosedPipe, io.ErrUnexpectedEOF:
|
||||||
eof = true
|
eof = true
|
||||||
default:
|
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
|
// Now assemble the fixed header
|
||||||
copy(fixHdr[:], []byte(_Magic))
|
copy(fixHdr[:], []byte(_Magic))
|
||||||
fixHdr[_MagicLen] = 1 // version #
|
fixHdr[_MagicLen] = _SigtoolVersion
|
||||||
binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize))
|
binary.BigEndian.PutUint32(fixHdr[_MagicLen+1:], uint32(varSize))
|
||||||
|
|
||||||
// Now marshal the variable portion
|
// Now marshal the variable portion
|
||||||
_, err := e.MarshalTo(varHdr[:varSize])
|
_, err := e.MarshalTo(varHdr[:varSize])
|
||||||
if err != nil {
|
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
|
// Now calculate checksum of everything
|
||||||
|
@ -246,7 +247,7 @@ func (e *Encryptor) start(wr io.Writer) error {
|
||||||
// Finally write it out
|
// Finally write it out
|
||||||
err = fullwrite(buffer, wr)
|
err = fullwrite(buffer, wr)
|
||||||
if err != nil {
|
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
|
// 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)
|
aes, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encrypt: %s", err)
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
|
ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
|
||||||
if err != nil {
|
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()))
|
e.buf = make([]byte, e.ChunkSize+4+uint32(ae.Overhead()))
|
||||||
|
@ -273,30 +274,14 @@ func (e *Encryptor) start(wr io.Writer) error {
|
||||||
return nil
|
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
|
// encrypt exactly _one_ block of data
|
||||||
// The nonce for the block is: sha256(salt || chunkLen || block#)
|
// The nonce for the block is: sha256(salt || chunkLen || block#)
|
||||||
// This protects the output stream from re-ordering attacks and length
|
// This protects the output stream from re-ordering attacks and length
|
||||||
// modification attacks. The encoded length & block number is used as
|
// modification attacks. The encoded length & block number is used as
|
||||||
// additional data in the AEAD construction.
|
// additional data in the AEAD construction.
|
||||||
func (e *Encryptor) encrypt(buf []byte, wr io.Writer, i uint32, eof bool) error {
|
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 z uint32 = uint32(len(buf))
|
||||||
|
var nbuf [_AEADNonceLen]byte
|
||||||
|
|
||||||
// mark last block
|
// mark last block
|
||||||
if eof {
|
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], z)
|
||||||
binary.BigEndian.PutUint32(b[4:], i)
|
binary.BigEndian.PutUint32(b[4:], i)
|
||||||
|
|
||||||
h := sha256.New()
|
nonce := makeNonceV2(nbuf[:], e.Salt, b)
|
||||||
h.Write(e.Salt)
|
|
||||||
h.Write(b[:])
|
|
||||||
nonce := h.Sum(nonceb[:0])[:e.ae.NonceSize()]
|
|
||||||
|
|
||||||
cbuf := e.buf[4:]
|
cbuf := e.buf[4:]
|
||||||
c := e.ae.Seal(cbuf[:0], nonce, buf, b[:])
|
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
|
n := len(c) + 4
|
||||||
err := fullwrite(e.buf[:n], wr)
|
err := fullwrite(e.buf[:n], wr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encrypt: %s", err)
|
return fmt.Errorf("encrypt: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -349,25 +331,27 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
||||||
|
|
||||||
_, err := io.ReadFull(rd, b[:])
|
_, err := io.ReadFull(rd, b[:])
|
||||||
if err != nil {
|
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 {
|
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 {
|
// Version check
|
||||||
return nil, fmt.Errorf("decrypt: Unsupported version %d", b[_MagicLen])
|
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:])
|
varSize := binary.BigEndian.Uint32(b[_MagicLen+1:])
|
||||||
|
|
||||||
// sanity check on variable segment length
|
// sanity check on variable segment length
|
||||||
if varSize > 1048576 {
|
if varSize > 1048576 {
|
||||||
return nil, fmt.Errorf("decrypt: header too large (max 1048576)")
|
return nil, ErrHeaderTooBig
|
||||||
}
|
}
|
||||||
if varSize < 32 {
|
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
|
// 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)
|
_, err = io.ReadFull(rd, varBuf)
|
||||||
if err != nil {
|
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:]
|
verify := varBuf[varSize:]
|
||||||
|
@ -386,7 +370,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
||||||
cksum := h.Sum(nil)
|
cksum := h.Sum(nil)
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 {
|
if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 {
|
||||||
return nil, fmt.Errorf("decrypt: header corrupted")
|
return nil, ErrBadHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
d := &Decryptor{
|
d := &Decryptor{
|
||||||
|
@ -396,7 +380,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
||||||
|
|
||||||
err = d.Unmarshal(varBuf[:varSize])
|
err = d.Unmarshal(varBuf[:varSize])
|
||||||
if err != nil {
|
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 {
|
if d.ChunkSize == 0 || d.ChunkSize >= maxChunkSize {
|
||||||
|
@ -408,7 +392,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(d.Keys) == 0 {
|
if len(d.Keys) == 0 {
|
||||||
return nil, fmt.Errorf("decrypt: no wrapped keys")
|
return nil, ErrNoWrappedKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanity check on the wrapped keys
|
// 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 {
|
for i, w := range d.Keys {
|
||||||
key, err = d.unwrapKey(w, sk)
|
key, err = d.unwrapKey(w, sk)
|
||||||
if err != nil {
|
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 {
|
if key != nil {
|
||||||
goto havekey
|
goto havekey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("decrypt: wrong key")
|
return ErrBadKey
|
||||||
|
|
||||||
havekey:
|
havekey:
|
||||||
if err := d.verifySender(key, sk, senderPk); err != nil {
|
if err := d.verifySender(key, sk, senderPk); err != nil {
|
||||||
return fmt.Errorf("decrypt: %s", err)
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.key = key
|
d.key = key
|
||||||
|
@ -455,12 +439,12 @@ havekey:
|
||||||
|
|
||||||
aes, err := aes.NewCipher(key)
|
aes, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("decrypt: %s", err)
|
return fmt.Errorf("decrypt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
|
d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
|
||||||
if err != nil {
|
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())
|
d.buf = make([]byte, int(d.ChunkSize)+d.ae.Overhead())
|
||||||
return nil
|
return nil
|
||||||
|
@ -475,11 +459,11 @@ func (d *Decryptor) AuthenticatedSender() bool {
|
||||||
// Decrypt the file and write to 'wr'
|
// Decrypt the file and write to 'wr'
|
||||||
func (d *Decryptor) Decrypt(wr io.Writer) error {
|
func (d *Decryptor) Decrypt(wr io.Writer) error {
|
||||||
if d.key == nil {
|
if d.key == nil {
|
||||||
return fmt.Errorf("decrypt: wrapped-key not decrypted (missing SetPrivateKey()?")
|
return ErrNoKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.stream {
|
if d.stream {
|
||||||
return fmt.Errorf("decrypt: can't use Decrypt() after using streaming I/O")
|
return ErrDecStarted
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.eof {
|
if d.eof {
|
||||||
|
@ -495,7 +479,7 @@ func (d *Decryptor) Decrypt(wr io.Writer) error {
|
||||||
if len(c) > 0 {
|
if len(c) > 0 {
|
||||||
err = fullwrite(c, wr)
|
err = fullwrite(c, wr)
|
||||||
if err != nil {
|
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
|
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
|
// Decrypt exactly one chunk of data
|
||||||
func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
||||||
var b [8]byte
|
var b [8]byte
|
||||||
|
@ -679,25 +526,192 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
binary.BigEndian.PutUint32(b[4:], i)
|
binary.BigEndian.PutUint32(b[4:], i)
|
||||||
h := sha256.New()
|
nonce := makeNonceV2(nonceb[:], d.Salt, b[:])
|
||||||
h.Write(d.Salt)
|
|
||||||
h.Write(b[:])
|
|
||||||
nonce := h.Sum(nonceb[:0])[:d.ae.NonceSize()]
|
|
||||||
|
|
||||||
z := m + ovh
|
z := m + ovh
|
||||||
n, err = io.ReadFull(d.rd, d.buf[:z])
|
n, err = io.ReadFull(d.rd, d.buf[:z])
|
||||||
if err != nil {
|
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[:])
|
p, err = d.ae.Open(d.buf[:0], nonce, d.buf[:n], b[:])
|
||||||
if err != nil {
|
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
|
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
|
// generate a KEK from a shared DH key and a Pub Key
|
||||||
func expand(shared, pk []byte) ([]byte, error) {
|
func expand(shared, pk []byte) ([]byte, error) {
|
||||||
kek := make([]byte, 32)
|
kek := make([]byte, 32)
|
||||||
|
@ -716,7 +730,8 @@ func newSender() (sk, pk []byte, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeNonce(v ...[]byte) []byte {
|
// do sha256 on a list of byte slices
|
||||||
|
func sha256Slices(v ...[]byte) []byte {
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
for _, x := range v {
|
for _, x := range v {
|
||||||
h.Write(x)
|
h.Write(x)
|
||||||
|
|
43
sign/errors.go
Normal file
43
sign/errors.go
Normal 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")
|
||||||
|
)
|
12
sign/ssh.go
12
sign/ssh.go
|
@ -25,7 +25,6 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -34,17 +33,6 @@ import (
|
||||||
"golang.org/x/crypto/ssh"
|
"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
|
const keySizeAES256 = 32
|
||||||
|
|
||||||
// ParseEncryptedRawPrivateKey returns a private key from an
|
// ParseEncryptedRawPrivateKey returns a private key from an
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,7 +96,7 @@ func (w *encWriter) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
w.n = 0
|
w.n = 0
|
||||||
w.err = errClosed
|
w.err = ErrClosed
|
||||||
return w.wr.Close()
|
return w.wr.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +111,7 @@ type encReader struct {
|
||||||
// NewStreamReader returns an io.Reader to read from the decrypted stream
|
// NewStreamReader returns an io.Reader to read from the decrypted stream
|
||||||
func (d *Decryptor) NewStreamReader() (io.Reader, error) {
|
func (d *Decryptor) NewStreamReader() (io.Reader, error) {
|
||||||
if d.key == nil {
|
if d.key == nil {
|
||||||
return nil, fmt.Errorf("streamReader: wrapped-key not decrypted (missing SetPrivateKey()?")
|
return nil, ErrNoKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.eof {
|
if d.eof {
|
||||||
|
@ -158,7 +156,3 @@ func (r *encReader) Read(b []byte) (int, error) {
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
errClosed = errors.New("encrypt: stream already closed")
|
|
||||||
)
|
|
||||||
|
|
1
version
1
version
|
@ -1 +0,0 @@
|
||||||
1.1.1
|
|
Loading…
Add table
Reference in a new issue