Add sender authenticated message integrity; fixup KDF

- use HKDF for producing keys, nonces
- add running hmac of plaintext; sender-sign the hmac as trailer
- use header checksum as "salt" for data encryption keys, nonces
- generate explicit nonce for wrapping root keys for each recipient
  (previous impl had brittleness)
This commit is contained in:
Sudhi Herle 2022-11-13 11:53:00 -08:00
parent a428db8feb
commit f343d45a8e
6 changed files with 495 additions and 249 deletions

View file

@ -34,7 +34,7 @@ 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"`
Pk []byte `protobuf:"bytes,3,opt,name=pk,proto3" json:"pk,omitempty"`
SenderSign []byte `protobuf:"bytes,4,opt,name=sender_sign,json=senderSign,proto3" json:"sender_sign,omitempty"`
Sender []byte `protobuf:"bytes,4,opt,name=sender,proto3" json:"sender,omitempty"`
Keys []*WrappedKey `protobuf:"bytes,5,rep,name=keys,proto3" json:"keys,omitempty"`
}
@ -91,9 +91,9 @@ func (m *Header) GetPk() []byte {
return nil
}
func (m *Header) GetSenderSign() []byte {
func (m *Header) GetSender() []byte {
if m != nil {
return m.SenderSign
return m.Sender
}
return nil
}
@ -110,6 +110,7 @@ func (m *Header) GetKeys() []*WrappedKey {
// key. WrappedKey describes such a wrapped key.
type WrappedKey struct {
DKey []byte `protobuf:"bytes,1,opt,name=d_key,json=dKey,proto3" json:"d_key,omitempty"`
Nonce []byte `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"`
}
func (m *WrappedKey) Reset() { *m = WrappedKey{} }
@ -151,6 +152,13 @@ func (m *WrappedKey) GetDKey() []byte {
return nil
}
func (m *WrappedKey) GetNonce() []byte {
if m != nil {
return m.Nonce
}
return nil
}
func init() {
proto.RegisterType((*Header)(nil), "pb.header")
proto.RegisterType((*WrappedKey)(nil), "pb.wrapped_key")
@ -159,24 +167,24 @@ func init() {
func init() { proto.RegisterFile("internal/pb/hdr.proto", fileDescriptor_c715362029a696e2) }
var fileDescriptor_c715362029a696e2 = []byte{
// 257 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0xcf, 0x31, 0x4e, 0xeb, 0x40,
0x10, 0xc6, 0xf1, 0x1d, 0xc7, 0x89, 0xf4, 0x26, 0x79, 0x20, 0x2d, 0x42, 0x72, 0xc3, 0x60, 0x99,
0xc6, 0x95, 0x23, 0x01, 0x27, 0xa0, 0xa5, 0x73, 0x0e, 0x60, 0xd9, 0x78, 0x14, 0x5b, 0x8e, 0x36,
0xab, 0xb5, 0x11, 0x72, 0x2a, 0x8e, 0x00, 0xb7, 0xe0, 0x28, 0x94, 0x2e, 0x53, 0xe2, 0x75, 0x43,
0x99, 0x23, 0x20, 0x2d, 0x0d, 0xdd, 0xa7, 0xdf, 0xbf, 0x99, 0xc1, 0xcb, 0x5a, 0x75, 0x6c, 0x54,
0xbe, 0x5b, 0xeb, 0x62, 0x5d, 0x95, 0x26, 0xd1, 0x66, 0xdf, 0xed, 0xa5, 0xa7, 0x8b, 0xe8, 0x1d,
0x70, 0x51, 0x71, 0x5e, 0xb2, 0x91, 0x57, 0x88, 0x4f, 0xd5, 0xb3, 0x6a, 0xb2, 0xb6, 0x3e, 0x70,
0x00, 0x21, 0xc4, 0xff, 0xd3, 0x7f, 0x4e, 0x36, 0xf5, 0x81, 0xa5, 0x44, 0xbf, 0xcd, 0x77, 0x5d,
0xe0, 0x85, 0x10, 0xaf, 0x52, 0xb7, 0xe5, 0x19, 0x7a, 0xba, 0x09, 0x66, 0x4e, 0x3c, 0xdd, 0xc8,
0x6b, 0x5c, 0xb6, 0xac, 0x4a, 0x36, 0x59, 0x5b, 0x6f, 0x55, 0xe0, 0xbb, 0x80, 0xbf, 0xb4, 0xa9,
0xb7, 0x4a, 0xde, 0xa0, 0xdf, 0x70, 0xdf, 0x06, 0xf3, 0x70, 0x16, 0x2f, 0x6f, 0xcf, 0x13, 0x5d,
0x24, 0x2f, 0x26, 0xd7, 0x9a, 0xcb, 0xac, 0xe1, 0x3e, 0x75, 0x31, 0x8a, 0x70, 0xf9, 0x07, 0xe5,
0x05, 0xce, 0xdd, 0x70, 0x27, 0xad, 0x52, 0xbf, 0x7c, 0xe4, 0xfe, 0xe1, 0x7e, 0x18, 0x49, 0x1c,
0x47, 0x12, 0xa7, 0x91, 0xe0, 0xd5, 0x12, 0x7c, 0x58, 0x82, 0x4f, 0x4b, 0x30, 0x58, 0x82, 0x2f,
0x4b, 0xf0, 0x6d, 0x49, 0x9c, 0x2c, 0xc1, 0xdb, 0x44, 0x62, 0x98, 0x48, 0x1c, 0x27, 0x12, 0xc5,
0xc2, 0x3d, 0x7e, 0xf7, 0x13, 0x00, 0x00, 0xff, 0xff, 0xb5, 0x7a, 0x39, 0x5a, 0x11, 0x01, 0x00,
0x00,
// 262 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0xd0, 0x3f, 0x4e, 0xf3, 0x40,
0x10, 0x05, 0xf0, 0x1d, 0xc7, 0x89, 0xf4, 0x4d, 0xf2, 0x81, 0xb4, 0xfc, 0x91, 0x1b, 0x46, 0x56,
0x68, 0x5c, 0x39, 0x12, 0x50, 0x50, 0xd3, 0xd2, 0x99, 0x03, 0x44, 0x76, 0x3c, 0x92, 0x2d, 0x47,
0xeb, 0xd5, 0xda, 0x08, 0x39, 0x15, 0x25, 0x25, 0xc7, 0xe0, 0x28, 0x94, 0x2e, 0x53, 0xe2, 0x75,
0x43, 0x99, 0x23, 0x20, 0x2d, 0x29, 0xe8, 0xde, 0xfb, 0x4d, 0xf3, 0x34, 0x78, 0x51, 0xaa, 0x96,
0x8d, 0x4a, 0xb7, 0x2b, 0x9d, 0xad, 0x8a, 0xdc, 0xc4, 0xda, 0xd4, 0x6d, 0x2d, 0x3d, 0x9d, 0x2d,
0xdf, 0x00, 0x67, 0x05, 0xa7, 0x39, 0x1b, 0x79, 0x85, 0xb8, 0x29, 0x9e, 0x55, 0xb5, 0x6e, 0xca,
0x1d, 0x07, 0x10, 0x42, 0xf4, 0x3f, 0xf9, 0xe7, 0xe4, 0xa9, 0xdc, 0xb1, 0x94, 0xe8, 0x37, 0xe9,
0xb6, 0x0d, 0xbc, 0x10, 0xa2, 0x45, 0xe2, 0xb2, 0x3c, 0x41, 0x4f, 0x57, 0xc1, 0xc4, 0x89, 0xa7,
0x2b, 0x79, 0x89, 0xb3, 0x86, 0x55, 0xce, 0x26, 0xf0, 0x9d, 0x1d, 0x9b, 0xbc, 0x46, 0xbf, 0xe2,
0xae, 0x09, 0xa6, 0xe1, 0x24, 0x9a, 0xdf, 0x9c, 0xc6, 0x3a, 0x8b, 0x5f, 0x4c, 0xaa, 0x35, 0xe7,
0xeb, 0x8a, 0xbb, 0xc4, 0x1d, 0x97, 0xf7, 0x38, 0xff, 0x83, 0xf2, 0x0c, 0xa7, 0x2e, 0xb8, 0x25,
0x8b, 0xc4, 0xcf, 0x1f, 0xb9, 0x93, 0xe7, 0x38, 0x55, 0xb5, 0xda, 0xf0, 0x71, 0xc5, 0x6f, 0x79,
0xb8, 0xeb, 0x07, 0x12, 0xfb, 0x81, 0xc4, 0x61, 0x20, 0x78, 0xb5, 0x04, 0x1f, 0x96, 0xe0, 0xd3,
0x12, 0xf4, 0x96, 0xe0, 0xcb, 0x12, 0x7c, 0x5b, 0x12, 0x07, 0x4b, 0xf0, 0x3e, 0x92, 0xe8, 0x47,
0x12, 0xfb, 0x91, 0x44, 0x36, 0x73, 0x5f, 0xb8, 0xfd, 0x09, 0x00, 0x00, 0xff, 0xff, 0x06, 0x40,
0x0b, 0xb4, 0x1e, 0x01, 0x00, 0x00,
}
func (this *Header) Equal(that interface{}) bool {
@ -207,7 +215,7 @@ func (this *Header) Equal(that interface{}) bool {
if !bytes.Equal(this.Pk, that1.Pk) {
return false
}
if !bytes.Equal(this.SenderSign, that1.SenderSign) {
if !bytes.Equal(this.Sender, that1.Sender) {
return false
}
if len(this.Keys) != len(that1.Keys) {
@ -242,6 +250,9 @@ func (this *WrappedKey) Equal(that interface{}) bool {
if !bytes.Equal(this.DKey, that1.DKey) {
return false
}
if !bytes.Equal(this.Nonce, that1.Nonce) {
return false
}
return true
}
func (this *Header) GoString() string {
@ -253,7 +264,7 @@ func (this *Header) GoString() string {
s = append(s, "ChunkSize: "+fmt.Sprintf("%#v", this.ChunkSize)+",\n")
s = append(s, "Salt: "+fmt.Sprintf("%#v", this.Salt)+",\n")
s = append(s, "Pk: "+fmt.Sprintf("%#v", this.Pk)+",\n")
s = append(s, "SenderSign: "+fmt.Sprintf("%#v", this.SenderSign)+",\n")
s = append(s, "Sender: "+fmt.Sprintf("%#v", this.Sender)+",\n")
if this.Keys != nil {
s = append(s, "Keys: "+fmt.Sprintf("%#v", this.Keys)+",\n")
}
@ -264,9 +275,10 @@ func (this *WrappedKey) GoString() string {
if this == nil {
return "nil"
}
s := make([]string, 0, 5)
s := make([]string, 0, 6)
s = append(s, "&pb.WrappedKey{")
s = append(s, "DKey: "+fmt.Sprintf("%#v", this.DKey)+",\n")
s = append(s, "Nonce: "+fmt.Sprintf("%#v", this.Nonce)+",\n")
s = append(s, "}")
return strings.Join(s, "")
}
@ -312,10 +324,10 @@ func (m *Header) MarshalToSizedBuffer(dAtA []byte) (int, error) {
dAtA[i] = 0x2a
}
}
if len(m.SenderSign) > 0 {
i -= len(m.SenderSign)
copy(dAtA[i:], m.SenderSign)
i = encodeVarintHdr(dAtA, i, uint64(len(m.SenderSign)))
if len(m.Sender) > 0 {
i -= len(m.Sender)
copy(dAtA[i:], m.Sender)
i = encodeVarintHdr(dAtA, i, uint64(len(m.Sender)))
i--
dAtA[i] = 0x22
}
@ -361,6 +373,13 @@ func (m *WrappedKey) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
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] = 0x12
}
if len(m.DKey) > 0 {
i -= len(m.DKey)
copy(dAtA[i:], m.DKey)
@ -399,7 +418,7 @@ func (m *Header) Size() (n int) {
if l > 0 {
n += 1 + l + sovHdr(uint64(l))
}
l = len(m.SenderSign)
l = len(m.Sender)
if l > 0 {
n += 1 + l + sovHdr(uint64(l))
}
@ -422,6 +441,10 @@ func (m *WrappedKey) Size() (n int) {
if l > 0 {
n += 1 + l + sovHdr(uint64(l))
}
l = len(m.Nonce)
if l > 0 {
n += 1 + l + sovHdr(uint64(l))
}
return n
}
@ -444,7 +467,7 @@ func (this *Header) String() string {
`ChunkSize:` + fmt.Sprintf("%v", this.ChunkSize) + `,`,
`Salt:` + fmt.Sprintf("%v", this.Salt) + `,`,
`Pk:` + fmt.Sprintf("%v", this.Pk) + `,`,
`SenderSign:` + fmt.Sprintf("%v", this.SenderSign) + `,`,
`Sender:` + fmt.Sprintf("%v", this.Sender) + `,`,
`Keys:` + repeatedStringForKeys + `,`,
`}`,
}, "")
@ -456,6 +479,7 @@ func (this *WrappedKey) String() string {
}
s := strings.Join([]string{`&WrappedKey{`,
`DKey:` + fmt.Sprintf("%v", this.DKey) + `,`,
`Nonce:` + fmt.Sprintf("%v", this.Nonce) + `,`,
`}`,
}, "")
return s
@ -586,7 +610,7 @@ func (m *Header) Unmarshal(dAtA []byte) error {
iNdEx = postIndex
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SenderSign", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
@ -613,9 +637,9 @@ func (m *Header) Unmarshal(dAtA []byte) error {
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.SenderSign = append(m.SenderSign[:0], dAtA[iNdEx:postIndex]...)
if m.SenderSign == nil {
m.SenderSign = []byte{}
m.Sender = append(m.Sender[:0], dAtA[iNdEx:postIndex]...)
if m.Sender == nil {
m.Sender = []byte{}
}
iNdEx = postIndex
case 5:
@ -736,6 +760,40 @@ func (m *WrappedKey) Unmarshal(dAtA []byte) error {
m.DKey = []byte{}
}
iNdEx = postIndex
case 2:
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
default:
iNdEx = preIndex
skippy, err := skipHdr(dAtA[iNdEx:])

View file

@ -19,7 +19,7 @@ message header {
uint32 chunk_size = 1; // encryption block size
bytes salt = 2; // master salt (nonces are derived from this)
bytes pk = 3; // ephemeral curve PK
bytes sender_sign = 4; // signature block of sender
bytes sender = 4; // sender signed artifacts
repeated wrapped_key keys = 5; // list of wrapped receiver blocks
}
@ -29,4 +29,6 @@ message header {
*/
message wrapped_key {
bytes d_key = 1; // encrypted data key
bytes nonce = 2; // nonce used for encryption
}

View file

@ -26,29 +26,36 @@
//
// Variable Length Segment:
// - Protobuf encoded, per-recipient wrapped keys
// - Shasum: 32 bytes (SHA256 of full header)
//
// The variable length segment consists of one or more
// recipients, each with their wrapped keys. This is encoded as
// a protobuf message. This protobuf encoded message immediately
// follows the fixed length header.
// recipients, each with their individually wrapped keys.
//
// The input data is encrypted with an expanded random 32-byte key:
// - Prefix_string = "Encrypt Nonce"
// - datakey = SHA256(Prefix_string || header_checksum || random_key)
// - The header checksum is mixed in the above process to ensure we
// catch any malicious modification of the header.
// - hkdf-sha512 of random key, salt, context
// - the hkdf process yields a data-encryption key, nonce and hmac key.
// - we use the header checksum as the 'salt' for HKDF; this ensures that
// any modification of the header yields different keys
//
// We also calculate the cumulative hmac-sha256 of the plaintext blocks.
// - When sender identity is present, we sign the final hmac and append
// the signature as the "trailer".
// - When sender identity is NOT present, we put random bytes as the
// "signature". ie in either case, there is a trailer.
//
// Note: If the trailer is missing from a sigtool encrypted file - the
// recipient has no guarantees of content immutability (ie tampering
// from one of the _other_ recipients).
//
// 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 = header.salt || block# || block-size
// AEAD nonce = header.nonce || block#
// AD of AEAD = chunk length+eof marker
//
// The encrypted block (includes the AEAD tag) length is written
// as a big-endian 4-byte prefix. The high-order bit of this length
// field is set for the last-block (denoting EOF).
//
// The encrypted blocks use an opinionated nonce length of 32 (_AEADNonceLen).
package sign
@ -57,6 +64,7 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
@ -64,42 +72,55 @@ import (
"fmt"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"hash"
"io"
"os"
"github.com/opencoff/sigtool/internal/pb"
)
// Encryption chunk size = 4MB
const (
chunkSize uint32 = 4 * 1048576
// The latest version of the tool's output file format
_SigtoolVersion = 3
chunkSize uint32 = 4 * 1048576 // 4 MB
maxChunkSize uint32 = 1 << 30
_EOF uint32 = 1 << 31
_Magic = "SigTool"
_MagicLen = len(_Magic)
_SigtoolVersion = 2
_AEADNonceLen = 32
_FixedHdrLen = _MagicLen + 1 + 4
_FixedHdrLen = _MagicLen + 1 + 4 // 1: version, 4: len of variable segment
_WrapReceiverNonce = "Receiver Key Nonce"
_WrapSenderNonce = "Sender Sig Nonce"
_EncryptNonce = "Encrypt Nonce"
_AesKeySize = 32
_AEADNonceSize = 12
_SaltSize = 32
_RxNonceSize = 12 // nonce size of per-recipient encrypted blocks
_WrapReceiver = "Receiver Key"
_WrapSender = "Sender Sig"
_DataKeyExpansion = "Data Key Expansion"
)
// Encryptor holds the encryption context
type Encryptor struct {
pb.Header
key []byte // file encryption key
key []byte // root key
nonce []byte // nonce for the data encrypting cipher
buf []byte // I/O buf (chunk-sized)
ae cipher.AEAD
hmac hash.Hash
// ephemeral key
encSK []byte
started bool
// sender identity
sender *PrivateKey
hdrsum []byte
buf []byte
auth bool // set if the sender idetity is sent
started bool
stream bool
}
@ -123,27 +144,23 @@ func NewEncryptor(sk *PrivateKey, blksize uint64) (*Encryptor, error) {
return nil, fmt.Errorf("encrypt: %w", err)
}
key := make([]byte, 32)
salt := make([]byte, _AEADNonceLen)
randRead(key)
randRead(salt)
wSig, err := wrapSenderSig(sk, key, salt)
if err != nil {
return nil, fmt.Errorf("encrypt: %w", err)
}
key := randBuf(_AesKeySize)
salt := randBuf(_SaltSize)
e := &Encryptor{
Header: pb.Header{
ChunkSize: blksz,
Salt: salt,
Pk: epk,
SenderSign: wSig,
},
key: key,
encSK: esk,
sender: sk,
}
if err = e.addSenderSig(sk); err != nil {
return nil, fmt.Errorf("encrypt: %w", err)
}
return e, nil
@ -210,8 +227,8 @@ func (e *Encryptor) start(wr io.Writer) error {
buffer := make([]byte, _FixedHdrLen+varSize+sha256.Size)
fixHdr := buffer[:_FixedHdrLen]
varHdr := buffer[_FixedHdrLen:]
sumHdr := varHdr[varSize:]
varHdr := buffer[_FixedHdrLen : _FixedHdrLen+varSize]
sumHdr := buffer[_FixedHdrLen+varSize:]
// Now assemble the fixed header
copy(fixHdr[:], []byte(_Magic))
@ -224,38 +241,45 @@ func (e *Encryptor) start(wr io.Writer) error {
return fmt.Errorf("encrypt: can't marshal header: %w", err)
}
// Now calculate checksum of everything
h := sha256.New()
h.Write(buffer[:_FixedHdrLen+varSize])
h.Sum(sumHdr[:0])
cksum := h.Sum(sumHdr[:0])
// Finally write it out
// now make the data encryption keys, nonces etc.
outbuf := make([]byte, sha256.Size+_AesKeySize+_AEADNonceSize)
// we mix the header checksum (and it captures the sigtool version, sender
// identity, etc.)
buf := expand(outbuf, e.key, cksum, []byte(_DataKeyExpansion))
var dkey, hmackey []byte
e.nonce, buf = buf[:_AEADNonceSize], buf[_AEADNonceSize:]
dkey, buf = buf[:_AesKeySize], buf[_AesKeySize:]
hmackey = buf
aes, err := aes.NewCipher(dkey)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
if e.ae, err = cipher.NewGCM(aes); err != nil {
return fmt.Errorf("encrypt: %w", err)
}
// Finally write out the header
err = fullwrite(buffer, wr)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
// we mix the header checksum to create the encryption key
h = sha256.New()
h.Write([]byte(_EncryptNonce))
h.Write(e.key)
h.Write(sumHdr)
key := h.Sum(nil)
aes, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
ae, err := cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
e.buf = make([]byte, e.ChunkSize+4+uint32(ae.Overhead()))
e.ae = ae
e.hmac = hmac.New(sha256.New, hmackey)
e.buf = make([]byte, e.ChunkSize+4+uint32(e.ae.Overhead()))
e.started = true
debug("encrypt:\n\thdr-cksum: %x\n\taes-key: %x\n\tnonce: %x\n\thmac-key: %x\n",
cksum, dkey, e.nonce, hmackey)
return nil
}
@ -264,30 +288,70 @@ func (e *Encryptor) start(wr io.Writer) error {
// 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 z uint32 = uint32(len(buf))
var nbuf [_AEADNonceLen]byte
func (e *Encryptor) encrypt(pt []byte, wr io.Writer, i uint32, eof bool) error {
var z uint32 = uint32(len(pt))
var nonce [_AEADNonceSize]byte
// mark last block
if eof {
z |= _EOF
}
b := e.buf[:8]
binary.BigEndian.PutUint32(b[:4], z)
binary.BigEndian.PutUint32(b[4:], i)
copy(nonce[:], e.nonce)
nonce := makeNonceV2(nbuf[:], e.Salt, b)
// now change the upper bytes to track the block#; we use the len+eof as AD
binary.BigEndian.PutUint32(nonce[:4], i)
cbuf := e.buf[4:]
c := e.ae.Seal(cbuf[:0], nonce, buf, b[:])
// put the encoded length+eof at the start of the output buf
b := e.buf[:4]
ctbuf := e.buf[4:]
binary.BigEndian.PutUint32(b, z)
ct := e.ae.Seal(ctbuf[:0], nonce[:], pt, b)
// total number of bytes written
n := len(c) + 4
n := len(ct) + 4
err := fullwrite(e.buf[:n], wr)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
e.hmac.Write(b)
e.hmac.Write(pt)
if eof {
return e.writeTrailer(wr)
}
return nil
}
// Write a trailer:
// - if authenticating sender, sign the hmac and put the signature in the trailer
// - if not authenticating sender, write random bytes to the trailer
func (e *Encryptor) writeTrailer(wr io.Writer) error {
var tr []byte
switch e.auth {
case true:
var hmac [sha256.Size]byte
e.hmac.Sum(hmac[:0])
// We know sender is non null.
sig, err := e.sender.SignMessage(hmac[:], "")
if err != nil {
return fmt.Errorf("encrypt: trailer: %w", err)
}
tr = sig.Sig
case false:
tr = randBuf(ed25519.SignatureSize)
}
if err := fullwrite(tr, wr); err != nil {
return fmt.Errorf("encrypt: trailer %w", err)
}
return nil
}
@ -296,15 +360,17 @@ type Decryptor struct {
pb.Header
ae cipher.AEAD
hmac hash.Hash
sender *PublicKey
rd io.Reader
buf []byte
hdrsum []byte
nonce []byte // nonce for the data decrypting cipher
// flag set to true if sender signed the key
auth bool
// Decrypted key
key []byte
key []byte // Decrypted root key
hdrsum []byte // cached header checksum
auth bool // flag set to true if sender signed the key
eof bool
stream bool
}
@ -347,14 +413,18 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
return nil, fmt.Errorf("decrypt: error while reading header: %w", err)
}
// The checksum in the header
verify := varBuf[varSize:]
// the checksum we calculated
var csum [sha256.Size]byte
h := sha256.New()
h.Write(b[:])
h.Write(varBuf[:varSize])
cksum := h.Sum(nil)
cksum := h.Sum(csum[:0])
if subtle.ConstantTimeCompare(verify, cksum[:]) == 0 {
if subtle.ConstantTimeCompare(verify, cksum) == 0 {
return nil, ErrBadHeader
}
@ -372,7 +442,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
return nil, fmt.Errorf("decrypt: invalid chunkSize %d", d.ChunkSize)
}
if len(d.Salt) != _AEADNonceLen {
if len(d.Salt) != _SaltSize {
return nil, fmt.Errorf("decrypt: invalid nonce length %d", len(d.Salt))
}
@ -382,7 +452,7 @@ func NewDecryptor(rd io.Reader) (*Decryptor, error) {
// sanity check on the wrapped keys
for i, w := range d.Keys {
if len(w.DKey) <= 32 {
if len(w.DKey) <= _AesKeySize {
return nil, fmt.Errorf("decrypt: wrapped key %d: wrong-size encrypted key", i)
}
}
@ -402,6 +472,8 @@ func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
return fmt.Errorf("decrypt: can't unwrap key %d: %w", i, err)
}
if key != nil {
d.key = key
d.sender = senderPk
goto havekey
}
}
@ -409,28 +481,37 @@ func (d *Decryptor) SetPrivateKey(sk *PrivateKey, senderPk *PublicKey) error {
return ErrBadKey
havekey:
if err := d.verifySender(key, sk, senderPk); err != nil {
if err := d.verifySender(key, senderPk); err != nil {
return fmt.Errorf("decrypt: %w", err)
}
d.key = key
outbuf := make([]byte, sha256.Size+_AesKeySize+_AEADNonceSize)
// we mix the header checksum into the key
h := sha256.New()
h.Write([]byte(_EncryptNonce))
h.Write(d.key)
h.Write(d.hdrsum)
key = h.Sum(nil)
buf := expand(outbuf, d.key, d.hdrsum, []byte(_DataKeyExpansion))
aes, err := aes.NewCipher(key)
var dkey, hmackey []byte
d.nonce, buf = buf[:_AEADNonceSize], buf[_AEADNonceSize:]
dkey, buf = buf[:_AesKeySize], buf[_AesKeySize:]
hmackey = buf
d.hmac = hmac.New(sha256.New, hmackey)
aes, err := aes.NewCipher(dkey)
if err != nil {
return fmt.Errorf("decrypt: %w", err)
}
d.ae, err = cipher.NewGCMWithNonceSize(aes, _AEADNonceLen)
d.ae, err = cipher.NewGCM(aes)
if err != nil {
return fmt.Errorf("decrypt: %w", err)
}
debug("decrypt:\n\thdr-cksum: %x\n\taes-key: %x\n\tnonce: %x\n\thmac-key: %x\n",
d.hdrsum, dkey, d.nonce, hmackey)
// We have a separate on-stack buffer for reading the header (4 bytes).
// Thus, the actual I/O buf will never be larger than the chunksize + AEAD Overhead
d.buf = make([]byte, int(d.ChunkSize)+d.ae.Overhead())
return nil
}
@ -477,17 +558,16 @@ func (d *Decryptor) Decrypt(wr io.Writer) error {
// Decrypt exactly one chunk of data
func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
var b [8]byte
var nonceb [32]byte
var ovh uint32 = uint32(d.ae.Overhead())
var p []byte
var b [4]byte
var nonce [_AEADNonceSize]byte
n, err := io.ReadFull(d.rd, b[:4])
n, err := io.ReadFull(d.rd, b[:])
if err != nil || n == 0 {
return nil, false, fmt.Errorf("decrypt: premature EOF while reading header block %d", i)
}
m := binary.BigEndian.Uint32(b[:4])
m := binary.BigEndian.Uint32(b[:])
eof := (m & _EOF) > 0
m &= (_EOF - 1)
@ -500,13 +580,14 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
if !eof {
return nil, false, fmt.Errorf("decrypt: block %d: zero-sized chunk without EOF", i)
}
return p, eof, nil
return nil, eof, nil
default:
}
binary.BigEndian.PutUint32(b[4:], i)
nonce := makeNonceV2(nonceb[:], d.Salt, b[:])
// make the nonce - top 4 bytes are the counter
copy(nonce[:], d.nonce)
binary.BigEndian.PutUint32(nonce[:4], i)
z := m + ovh
n, err = io.ReadFull(d.rd, d.buf[:z])
@ -514,57 +595,115 @@ func (d *Decryptor) decrypt(i uint32) ([]byte, bool, error) {
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[:])
pt, 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: %w", i, err)
}
return p[:m], eof, nil
if uint32(len(pt)) != m {
return nil, false, fmt.Errorf("decrypt: partial unsealed bytes; exp %d, saw %d", m, len(pt))
}
// Wrap sender's signature of the encryption key
// if sender has provided their identity to authenticate, we sign the data-enc key
// and encrypt the signature. At no point will we send the sender's identity.
func wrapSenderSig(sk *PrivateKey, key, salt []byte) ([]byte, error) {
d.hmac.Write(b[:])
d.hmac.Write(pt)
if eof {
return d.processTrailer(pt, eof)
}
return pt, eof, nil
}
func (d *Decryptor) processTrailer(pt []byte, eof bool) ([]byte, bool, error) {
var rd [ed25519.SignatureSize]byte
_, err := io.ReadFull(d.rd, rd[:])
if err != nil {
return nil, false, fmt.Errorf("decrypt: premature EOF while reading trailer: %w", err)
}
if !d.auth {
// these are random bytes; ignore em
return pt, eof, nil
}
var hmac [sha256.Size]byte
cksum := d.hmac.Sum(hmac[:0])
ss := &Signature{
Sig: rd[:],
}
if ok := d.sender.VerifyMessage(cksum, ss); !ok {
return nil, eof, ErrBadTrailer
}
return pt, eof, nil
}
// optionally sign the checksum and encrypt everything
func (e *Encryptor) addSenderSig(sk *PrivateKey) error {
var zero [ed25519.SignatureSize]byte
var sig []byte
var auth bool
sig := zero[:]
switch {
case sk == nil:
sig = zero[:]
if e.sender != nil {
var csum [sha256.Size]byte
default:
xsig, err := sk.SignMessage(key, "")
// We capture essential meta-data from the sender; viz:
// - Sender tool version
// - Sender generated curve25519 PK
// - session salt, root key
h := sha256.New()
h.Write([]byte(_Magic))
h.Write([]byte{_SigtoolVersion})
h.Write(e.Pk)
h.Write(e.Salt)
h.Write(e.key)
cksum := h.Sum(csum[:0])
xsig, err := e.sender.SignMessage(cksum, "")
if err != nil {
return nil, fmt.Errorf("wrap: can't sign: %w", err)
return fmt.Errorf("wrap: can't sign: %w", err)
}
sig = xsig.Sig
auth = true
}
aes, err := aes.NewCipher(key)
buf := make([]byte, _AesKeySize+_AEADNonceSize)
buf = expand(buf, e.key, e.Salt, []byte(_WrapSender))
ekey, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
aes, err := aes.NewCipher(ekey)
if err != nil {
return nil, fmt.Errorf("wrap: %w", err)
return fmt.Errorf("senderId: %w", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("wrap: %w", err)
return fmt.Errorf("senderId: %w", err)
}
tagsize := ae.Overhead()
nonceSize := ae.NonceSize()
outbuf := make([]byte, ed25519.SignatureSize+ae.Overhead())
buf = ae.Seal(outbuf[:0], nonce, sig, nil)
nonce := sha256Slices([]byte(_WrapSenderNonce), salt)[:nonceSize]
esig := make([]byte, tagsize+len(sig))
e.auth = auth
e.Sender = buf
return ae.Seal(esig[:0], nonce, sig, nil), nil
return 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)
func (d *Decryptor) verifySender(key []byte, senderPk *PublicKey) error {
outbuf := make([]byte, _AEADNonceSize+_AesKeySize)
buf := expand(outbuf, key, d.Salt, []byte(_WrapSender))
ekey, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
aes, err := aes.NewCipher(ekey)
if err != nil {
return fmt.Errorf("unwrap: %w", err)
}
@ -574,31 +713,42 @@ func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey
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)
var sigbuf [ed25519.SignatureSize]byte
var zero [ed25519.SignatureSize]byte
sig, err := ae.Open(sigbuf[:0], nonce, d.Sender, 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 {
// we set this to indicate that the sender authenticated themselves;
d.auth = true
if senderPk == nil {
return ErrNoSenderPK
}
var csum [sha256.Size]byte
h := sha256.New()
h.Write([]byte(_Magic))
h.Write([]byte{_SigtoolVersion})
h.Write(d.Pk)
h.Write(d.Salt)
h.Write(key)
cksum := h.Sum(csum[:0])
if senderPK != nil {
ss := &Signature{
Sig: sig,
}
if ok := senderPK.VerifyMessage(key, ss); !ok {
return fmt.Errorf("unwrap: sender verification failed")
}
if ok := senderPk.VerifyMessage(cksum, ss); !ok {
return ErrBadSender
}
// we set this to indicate that the sender authenticated themselves;
d.auth = true
}
return nil
}
@ -606,12 +756,26 @@ func (d *Decryptor) verifySender(key []byte, sk *PrivateKey, senderPK *PublicKey
// basically, we do a scalarmult: Ephemeral encryption/decryption SK x receiver PK
func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
rxPK := pk.ToCurve25519PK()
dkek, err := curve25519.X25519(e.encSK, rxPK)
sekrit, err := curve25519.X25519(e.encSK, rxPK)
if err != nil {
return nil, fmt.Errorf("wrap: %w", err)
}
aes, err := aes.NewCipher(dkek)
var shasum [sha256.Size]byte
rbuf := randBuf(_RxNonceSize)
h := sha256.New()
h.Write(e.Salt)
h.Write(rbuf[:])
h.Sum(shasum[:0])
out := make([]byte, _AesKeySize+_RxNonceSize)
buf := expand(out[:], sekrit, shasum[:], []byte(_WrapReceiver))
kek, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
aes, err := aes.NewCipher(kek)
if err != nil {
return nil, fmt.Errorf("wrap: %w", err)
}
@ -621,14 +785,10 @@ func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
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))
ekey := make([]byte, ae.Overhead()+len(e.key))
w := &pb.WrappedKey{
DKey: ae.Seal(ekey[:0], nonceR, e.key, pk.Pk),
DKey: ae.Seal(ekey[:0], nonce, e.key, pk.Pk),
Nonce: rbuf,
}
return w, nil
@ -638,40 +798,48 @@ func (e *Encryptor) wrapKey(pk *PublicKey) (*pb.WrappedKey, error) {
// senders ephemeral PublicKey
func (d *Decryptor) unwrapKey(w *pb.WrappedKey, sk *PrivateKey) ([]byte, error) {
ourSK := sk.ToCurve25519SK()
dkek, err := curve25519.X25519(ourSK, d.Pk)
sekrit, err := curve25519.X25519(ourSK, d.Pk)
if err != nil {
return nil, fmt.Errorf("unwrap: %w", err)
}
aes, err := aes.NewCipher(dkek)
var shasum [sha256.Size]byte
h := sha256.New()
h.Write(d.Salt)
h.Write(w.Nonce)
h.Sum(shasum[:0])
out := make([]byte, _AesKeySize+_RxNonceSize)
buf := expand(out[:], sekrit, shasum[:], []byte(_WrapReceiver))
kek, nonce := buf[:_AesKeySize], buf[_AesKeySize:]
aes, err := aes.NewCipher(kek)
if err != nil {
return nil, fmt.Errorf("unwrap: %w", err)
return nil, fmt.Errorf("wrap: %w", err)
}
ae, err := cipher.NewGCM(aes)
if err != nil {
return nil, fmt.Errorf("unwrap: %w", err)
return nil, fmt.Errorf("wrap: %w", err)
}
// 32 == AES-256 key size
want := 32 + ae.Overhead()
want := _AesKeySize + 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
dkey := make([]byte, _AesKeySize) // 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)
dkey, err = ae.Open(dkey[:0], nonce, w.DKey, pk.Pk)
if err != nil {
return nil, nil
}
// we have successfully found the correct recipient
return dkey, nil
}
@ -691,30 +859,14 @@ func fullwrite(buf []byte, wr io.Writer) error {
return nil
}
// make aead nonce from salt, chunk-size and block#
// First 8 bytes are chunk-size and nonce (in 'ad')
func makeNonceV2(dest []byte, salt []byte, ad []byte) []byte {
n := len(ad)
copy(dest, ad)
copy(dest[n:], salt)
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)
h := hkdf.New(sha512.New, shared, pk, nil)
_, err := io.ReadFull(h, kek)
return kek, err
func expand(out []byte, shared, salt, ad []byte) []byte {
h := hkdf.New(sha512.New, shared, salt, ad)
_, err := io.ReadFull(h, out)
if err != nil {
panic(fmt.Sprintf("hkdf: failed to generate %d bytes: %s", len(out), err))
}
return out
}
func newSender() (sk, pk []byte, err error) {
@ -736,4 +888,25 @@ func sha256Slices(v ...[]byte) []byte {
return h.Sum(nil)[:]
}
var _debug int = 0
// Enable debugging of this module;
// level > 0 elicits debug messages on os.Stderr
func Debug(level int) {
_debug = level
}
func debug(s string, v ...interface{}) {
if _debug <= 0 {
return
}
z := fmt.Sprintf(s, v...)
if n := len(z); z[n-1] != '\n' {
z += "\n"
}
os.Stderr.WriteString(z)
os.Stderr.Sync()
}
// EOF

View file

@ -20,17 +20,19 @@ import (
var (
ErrClosed = errors.New("encrypt: stream already closed")
ErrNoKey = errors.New("decrypt: No private key set for decryption")
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?")
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")
ErrNoWrappedKeys = errors.New("decrypt: no wrapped keys in encrypted file")
ErrBadKey = errors.New("decrypt: wrong key")
ErrBadTrailer = errors.New("decrypt: message integrity failed (bad trailer)")
ErrBadSender = errors.New("unwrap: sender verification failed")
ErrNoSenderPK = errors.New("unwrap: missing sender public key")
ErrIncorrectPassword = errors.New("ssh: invalid passphrase")
ErrNoPEMFound = errors.New("ssh: no PEM block found")

View file

@ -38,3 +38,8 @@ func randRead(b []byte) []byte {
}
return b
}
func randBuf(sz int) []byte {
b := make([]byte, sz)
return randRead(b)
}

View file

@ -30,12 +30,13 @@ var Z string = path.Base(os.Args[0])
func main() {
var ver, help bool
var ver, help, debug bool
mf := flag.NewFlagSet(Z, flag.ExitOnError)
mf.SetInterspersed(false)
mf.BoolVarP(&ver, "version", "v", false, "Show version info and exit")
mf.BoolVarP(&help, "help", "h", false, "Show help info exit")
mf.BoolVarP(&debug, "debug", "", false, "Enable debug mode")
mf.Parse(os.Args[1:])
if ver {
@ -80,6 +81,10 @@ func main() {
Die("can't map command %s", canon)
}
if debug {
sign.Debug(1)
}
cmd(args[1:])
// always call Exit so that at-exit handlers are called.
@ -323,7 +328,8 @@ Usage: %s [global-options] command [options] arg [args..]
Global options:
-h, --help Show help and exit
-v, --version Show version info and exit.
-v, --version Show version info and exit
--debug Enable debug (DANGEROUS)
Commands:
generate, g Generate a new Ed25519 keypair