github.com/creachadair/ffs@v0.17.3/storage/codecs/encrypted/encrypted.go (about)

     1  // Copyright 2019 Michael J. Fromberger. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package encrypted implements an encryption codec which encodes data by
    16  // encrypting and authenticating with a [cipher.AEAD] instance.
    17  package encrypted
    18  
    19  import (
    20  	"crypto/cipher"
    21  	crand "crypto/rand"
    22  	"encoding/binary"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  
    27  	"github.com/golang/snappy"
    28  )
    29  
    30  // A Codec implements the [encoded.Codec] interface and encrypts and
    31  // authenticates data using a [cipher.AEAD] instance.
    32  //
    33  // [encoded.Codec]: https://godoc.org/github.com/creachadair/ffs/storage/encoded#Codec
    34  type Codec struct {
    35  	newCipher func([]byte) (cipher.AEAD, error)
    36  	keys      Keyring
    37  }
    38  
    39  // New constructs an encryption codec that uses the given encryption context.
    40  // The newCipher function constructs a [cipher.AEAD] from an encryption key.
    41  // The keys argument is a collection of encryption keys.
    42  // Both newCipher and keys must be non-nil.
    43  func New(newCipher func([]byte) (cipher.AEAD, error), keys Keyring) *Codec {
    44  	switch {
    45  	case newCipher == nil:
    46  		panic("cipher constructor is nil")
    47  	case keys == nil:
    48  		panic("keyring is nil")
    49  	}
    50  	return &Codec{newCipher: newCipher, keys: keys}
    51  }
    52  
    53  // Encode encrypts src with the current active key in the provided keyring,
    54  // and writes the result to w.
    55  func (c *Codec) Encode(w io.Writer, src []byte) error { return c.encrypt(w, src) }
    56  
    57  // Decode decrypts src with the key ID used to encrypt it, and writes the
    58  // result to w.
    59  func (c *Codec) Decode(w io.Writer, src []byte) error {
    60  	blk, err := parseBlock(src)
    61  	if err != nil {
    62  		return err
    63  	}
    64  	return c.decrypt(w, blk)
    65  }
    66  
    67  // encrypt compresses and encrypts the given data and writes it to w.
    68  func (c *Codec) encrypt(w io.Writer, data []byte) error {
    69  	var kbuf [64]byte
    70  	id, key := c.keys.GetActive(kbuf[:0])
    71  
    72  	aead, err := c.newCipher(key)
    73  	if err != nil {
    74  		return err
    75  	}
    76  	nlen := aead.NonceSize()
    77  
    78  	// Preallocate a buffer for the result:
    79  	//   size: [   1   |    4    |     nlen     | data ...        ]
    80  	//   desc:    tag     keyID       nonce       payload ...
    81  	//
    82  	// Where tag == 128+nlen.
    83  	//
    84  	// The plaintext is compressed, which may expand it.
    85  	// In addition, the AEAD adds additional data for extra data and message authentication.
    86  	// The buffer must be large enough to hold all of these.
    87  	bufSize := 1 + 4 + nlen + snappy.MaxEncodedLen(len(data)) + aead.Overhead()
    88  
    89  	buf := make([]byte, bufSize)
    90  	keyID, nonce, payload := buf[1:5], buf[5:5+nlen], buf[5+nlen:]
    91  
    92  	buf[0] = byte(nlen) | 0x80 // tag
    93  	binary.BigEndian.PutUint32(keyID, uint32(id))
    94  	crand.Read(nonce) // panics on error
    95  
    96  	// Compress the plaintext into the buffer after the nonce, then encrypt the
    97  	// compressed data in-place. Both of these will change the length of the
    98  	// afflicted buffer segment, so we then have to reslice the buffer to get
    99  	// the final packet.
   100  	compressed := snappy.Encode(payload, data)
   101  	encrypted := aead.Seal(compressed[:0], nonce, compressed, nil)
   102  	outLen := 1 + 4 + nlen + len(encrypted)
   103  	_, err = w.Write(buf[:outLen])
   104  	return err
   105  }
   106  
   107  // decrypt decrypts and decompresses the data from a storage wrapper.
   108  func (c *Codec) decrypt(w io.Writer, blk block) error {
   109  	if !c.keys.Has(blk.KeyID) {
   110  		return fmt.Errorf("key id %d not found", blk.KeyID)
   111  	}
   112  	var kbuf [64]byte
   113  	key := c.keys.Get(blk.KeyID, kbuf[:0])
   114  	aead, err := c.newCipher(key)
   115  	if err != nil {
   116  		return err
   117  	}
   118  
   119  	plain, err := aead.Open(make([]byte, 0, len(blk.Data)), blk.Nonce, blk.Data, nil)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	dlen, err := snappy.DecodedLen(plain)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	decompressed, err := snappy.Decode(make([]byte, dlen), plain)
   128  	if err != nil {
   129  		return fmt.Errorf("decompress: %w", err)
   130  	}
   131  	_, err = w.Write(decompressed)
   132  	return err
   133  }
   134  
   135  type block struct {
   136  	KeyID int
   137  	Nonce []byte
   138  	Data  []byte
   139  }
   140  
   141  // parseBlock parses the binary encoding of a block, reporting an error if the
   142  // structure of the block is invalid.
   143  func parseBlock(from []byte) (block, error) {
   144  	if len(from) == 0 {
   145  		return block{}, errors.New("parse: invalid block format")
   146  	}
   147  	hasKeyID := from[0]&0x80 != 0
   148  	nonceLen := int(from[0] & 0x7f)
   149  	if hasKeyID {
   150  		if len(from) < 5+nonceLen {
   151  			return block{}, errors.New("parse: truncated block")
   152  		}
   153  		return block{
   154  			KeyID: int(binary.BigEndian.Uint32(from[1:])),
   155  			Nonce: from[5 : 5+nonceLen],
   156  			Data:  from[5+nonceLen:],
   157  		}, nil
   158  	}
   159  	return block{
   160  		KeyID: 1,
   161  		Nonce: from[1 : 1+nonceLen],
   162  		Data:  from[1+nonceLen:],
   163  	}, nil
   164  }
   165  
   166  /*
   167  Implementation notes
   168  
   169  The original format of the encrypted block was:
   170  
   171     Pos | Len  | Description
   172     ----|------|------------------------------
   173     0   | 1    | nonce length in bytes (= n)
   174     1   | n    | AEAD nonce
   175     n+1 | rest | encrypted compressed data
   176  
   177  The current format of the encrypted block is:
   178  
   179     Pos | Len  | Description
   180     ----|------|------------------------------
   181     0   | 1    | nonce length in byte (= n+128)
   182     1   | 4    | key ID (BE uint32 = id)
   183     5   | n    | AEAD nonce
   184     n+5 | rest | encrypted compressed data
   185  
   186  The two can be distinguished by checking the high-order bit of the first byte
   187  of the stored data. This requires that the actual nonce length is < 128, which
   188  it will be in all practical use.
   189  
   190  Block data are compressed before encryption (decompressed after decryption)
   191  with https://github.com/google/snappy.
   192  */
   193  
   194  // Keyring is the interface used to fetch encryption keys.
   195  type Keyring interface {
   196  	// Has reports whether the keyring contains a key with the given ID.
   197  	Has(id int) bool
   198  
   199  	// Get appends the contents of the specified key to buf, and returns the
   200  	// resulting slice.
   201  	Get(id int, buf []byte) []byte
   202  
   203  	// GetActive appends the contents of the active key to buf, and returns
   204  	// active ID and the updated slice.
   205  	GetActive(buf []byte) (int, []byte)
   206  }