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 }