blob: 00cb16b1d89f850910ffe723c49841a8d3cae787 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
// Package streamingaead provides subtle implementations of the streaming AEAD primitive.
package streamingaead
import (
"crypto/aes"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/google/tink/go/subtle/aead"
"github.com/google/tink/go/subtle/random"
"github.com/google/tink/go/subtle"
"github.com/google/tink/go/tink"
)
const (
// nonceSizeInBytes is the size of the IVs for GCM.
nonceSizeInBytes = 12
// NoncePrefixInBytes is the nonce has the format nonce_prefix || ctr || last_block.
// The nonce_prefix is constant for the whole file.
// The ctr is a 32 bit ctr, the last_block is 1 if this is the
// last block of the file and 0 otherwise.
NoncePrefixInBytes = 7
// TagSizeInBytes is the size of the tags of each ciphertext segment.
TagSizeInBytes = 16
)
var _ tink.StreamingAEAD = &AESGCMHKDF{}
// AESGCMHKDF implements streaming encryption using AES-GCM with HKDF as key derivation function.
//
// Each ciphertext uses a new AES-GCM key that is derived from the key derivation key, a randomly
// chosen salt of the same size as the key and a nonce prefix.
//
// The format of a ciphertext is header || segment_0 || segment_1 || ... || segment_k. The
// header has size HeaderLength(). Its format is headerLength || salt || prefix. where
// headerLength is 1 byte determining the size of the header, salt is a salt used in the key
// derivation and prefix is the prefix of the nonce. In principle headerLength is redundant
// information, since the length of the header can be determined from the key size.
//
// segment_i is the i-th segment of the ciphertext. The size of segment_1 .. segment_{k-1} is
// ciphertextSegmentSize. segment_0 is shorter, so that segment_0, the header and other information
// of size firstSegmentOffset align with ciphertextSegmentSize.
type AESGCMHKDF struct {
MainKey []byte
hkdfAlg string
keySizeInBytes int
ciphertextSegmentSize int
firstCiphertextSegmentOffset int
plaintextSegmentSize int
}
// NewAESGCMHKDF initializes a streaming primitive with a key derivation key and encryption parameters.
//
// mainKey argument is an input keying material used to derive sub keys.
// hkdfAlg argument is a JCE MAC algorithm name, e.g., HmacSha256, used for the HKDF key derivation.
// keySizeInBytes argument is a key size of the sub keys
// ciphertextSegmentSize argument is the size of ciphertext segments.
// firstSegmentOffset argument is the offset of the first ciphertext segment. That means the first
// segment has size ciphertextSegmentSize - HeaderLength() - firstSegmentOffset
func NewAESGCMHKDF(
mainKey []byte,
hkdfAlg string,
keySizeInBytes int,
ciphertextSegmentSize int,
firstSegmentOffset int,
) (*AESGCMHKDF, error) {
if len(mainKey) < 16 || len(mainKey) < keySizeInBytes {
return nil, errors.New("mainKey too short")
}
if err := aead.ValidateAESKeySize(uint32(keySizeInBytes)); err != nil {
return nil, err
}
headerLen := 1 + keySizeInBytes + NoncePrefixInBytes
if ciphertextSegmentSize <= firstSegmentOffset+headerLen+TagSizeInBytes {
return nil, errors.New("ciphertextSegmentSize too small")
}
keyClone := make([]byte, len(mainKey))
copy(keyClone, mainKey)
return &AESGCMHKDF{
MainKey: keyClone,
hkdfAlg: hkdfAlg,
keySizeInBytes: keySizeInBytes,
ciphertextSegmentSize: ciphertextSegmentSize,
firstCiphertextSegmentOffset: firstSegmentOffset + headerLen,
plaintextSegmentSize: ciphertextSegmentSize - TagSizeInBytes,
}, nil
}
// HeaderLength returns a length of the encryption header.
func (a *AESGCMHKDF) HeaderLength() int {
return 1 + a.keySizeInBytes + NoncePrefixInBytes
}
// deriveKey returns a key derived from the given main key using salt and aad parameters.
func (a *AESGCMHKDF) deriveKey(salt, aad []byte) ([]byte, error) {
return subtle.ComputeHKDF(a.hkdfAlg, a.MainKey, salt, aad, uint32(a.keySizeInBytes))
}
// aesGCMHKDFWriter works as a wrapper around underlying io.Writer, which is responsible for
// encrypting written data. The data is encrypted and flushed in segments of a given size.
// Once all the data is written aesGCMHKDFWriter must be closed.
type aesGCMHKDFWriter struct {
encryptedSegments int
noncePrefix []byte
cipher cipher.AEAD
wr io.Writer
pt []byte
ptPos int
ct []byte
firstCiphertextSegmentOffset int
closed bool
}
// NewEncryptingWriter returns a wrapper around underlying io.Writer, such that any write-operation
// via the wrapper results in AEAD-encryption of the written data, using aad
// as associated authenticated data. The associated data is not included in the ciphertext
// and has to be passed in as parameter for decryption.
func (a *AESGCMHKDF) NewEncryptingWriter(w io.Writer, aad []byte) (io.WriteCloser, error) {
salt := random.GetRandomBytes(uint32(a.keySizeInBytes))
noncePrefix := random.GetRandomBytes(NoncePrefixInBytes)
dkey, err := a.deriveKey(salt, aad)
if err != nil {
return nil, err
}
cipher, err := newCipher(dkey)
if err != nil {
return nil, err
}
header := make([]byte, a.HeaderLength())
header[0] = byte(a.HeaderLength())
copy(header[1:], salt)
copy(header[1+len(salt):], noncePrefix)
if _, err := w.Write(header); err != nil {
return nil, err
}
return &aesGCMHKDFWriter{
noncePrefix: noncePrefix,
cipher: cipher,
wr: w,
pt: make([]byte, a.plaintextSegmentSize),
firstCiphertextSegmentOffset: a.firstCiphertextSegmentOffset,
}, nil
}
// Write encrypts passed data and passes the encrypted data to the underlying writer.
func (w *aesGCMHKDFWriter) Write(p []byte) (int, error) {
if w.closed {
return 0, errors.New("write on closed writer")
}
pos := 0
for {
ptLim := len(w.pt)
if w.encryptedSegments == 0 {
ptLim = len(w.pt) - w.firstCiphertextSegmentOffset
}
n := copy(w.pt[w.ptPos:ptLim], p[pos:])
w.ptPos += n
pos += n
if pos == len(p) {
break
}
nonce := generateSegmentNonce(w.noncePrefix, w.encryptedSegments, false)
w.ct = w.cipher.Seal(w.ct[0:0], nonce, w.pt[:ptLim], nil)
if _, err := w.wr.Write(w.ct); err != nil {
return pos, err
}
w.ptPos = 0
w.encryptedSegments++
}
return pos, nil
}
// Close encrypts the remaining data, flushes it to the underlying writer and closes this writer.
func (w *aesGCMHKDFWriter) Close() error {
if w.closed {
return nil
}
nonce := generateSegmentNonce(w.noncePrefix, w.encryptedSegments, true)
w.ct = w.cipher.Seal(w.ct[0:0], nonce, w.pt[0:w.ptPos], nil)
if _, err := w.wr.Write(w.ct); err != nil {
return err
}
w.ptPos = 0
w.encryptedSegments++
w.closed = true
return nil
}
// aesGCMHKDFReader works as a wrapper around underlying io.Reader.
type aesGCMHKDFReader struct {
decryptedSegments int
noncePrefix []byte
cipher cipher.AEAD
underlyingReader io.Reader
pt []byte
ptPos int
ct []byte
ctPos int
firstSegmentOffset int
}
// NewDecryptingReader returns a wrapper around underlying io.Reader, such that any read-operation
// via the wrapper results in AEAD-decryption of the underlying ciphertext,
// using aad as associated authenticated data.
func (a *AESGCMHKDF) NewDecryptingReader(r io.Reader, aad []byte) (io.Reader, error) {
hlen := make([]byte, 1)
if _, err := io.ReadFull(r, hlen); err != nil {
return nil, err
}
if hlen[0] != byte(a.HeaderLength()) {
return nil, errors.New("invalid header length")
}
salt := make([]byte, a.keySizeInBytes)
if _, err := io.ReadFull(r, salt); err != nil {
return nil, fmt.Errorf("cannot read salt: %v", err)
}
noncePrefix := make([]byte, NoncePrefixInBytes)
if _, err := io.ReadFull(r, noncePrefix); err != nil {
return nil, fmt.Errorf("cannot read noncePrefix: %v", err)
}
dkey, err := a.deriveKey(salt, aad)
if err != nil {
return nil, err
}
cipher, err := newCipher(dkey)
if err != nil {
return nil, err
}
return &aesGCMHKDFReader{
noncePrefix: noncePrefix,
cipher: cipher,
underlyingReader: r,
// Allocate an extra byte to detect last segment.
ct: make([]byte, a.ciphertextSegmentSize+1),
firstSegmentOffset: a.firstCiphertextSegmentOffset,
}, nil
}
// Read decrypts data from underlying reader and passes it to p.
func (r *aesGCMHKDFReader) Read(p []byte) (int, error) {
if r.ptPos < len(r.pt) {
n := copy(p, r.pt[r.ptPos:])
r.ptPos += n
return n, nil
}
ctLim := len(r.ct)
if r.decryptedSegments == 0 {
ctLim -= r.firstSegmentOffset
}
n, err := io.ReadFull(r.underlyingReader, r.ct[r.ctPos:ctLim])
if err != nil && err != io.ErrUnexpectedEOF {
return 0, err
}
var (
lastSegment bool
segment int
)
if err != nil {
lastSegment = true
segment = r.ctPos + n
} else {
segment = r.ctPos + n - 1
}
nonce := generateSegmentNonce(r.noncePrefix, r.decryptedSegments, lastSegment)
r.pt, err = r.cipher.Open(r.pt[0:0], nonce, r.ct[:segment], nil)
if err != nil {
return 0, err
}
// Copy 1 byte remainder to the beginning of ct.
if !lastSegment {
r.ct[0] = r.ct[segment]
r.ctPos = 1
}
r.decryptedSegments++
r.ptPos = 0
n = copy(p, r.pt)
r.ptPos = n
return n, nil
}
// newCipher creates a new AES-GCM cipher using the given key and the crypto library.
func newCipher(key []byte) (cipher.AEAD, error) {
aesCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
ret, err := cipher.NewGCMWithTagSize(aesCipher, TagSizeInBytes)
if err != nil {
return nil, err
}
return ret, nil
}
func generateSegmentNonce(noncePrefix []byte, segmentNr int, last bool) []byte {
var l byte
if last {
l = 1
}
nonce := make([]byte, nonceSizeInBytes)
offs := 0
copy(nonce, noncePrefix)
offs += len(noncePrefix)
binary.BigEndian.PutUint32(nonce[offs:], uint32(segmentNr))
offs += 4
nonce[offs] = l
return nonce
}