1// Copyright 2020 Google LLC 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//////////////////////////////////////////////////////////////////////////////// 16 17package noncebased_test 18 19import ( 20 "bufio" 21 "bytes" 22 "crypto/rand" 23 "encoding/hex" 24 "errors" 25 "fmt" 26 "io" 27 "testing" 28 29 "github.com/google/tink/go/streamingaead/subtle/noncebased" 30) 31 32func TestNonceBased(t *testing.T) { 33 34 testcases := []struct { 35 name string 36 plaintextSize int 37 nonceSize int 38 noncePrefixSize int 39 plaintextSegmentSize int 40 firstCiphertextSegmentOffset int 41 chunkSize int 42 }{ 43 { 44 name: "plaintextSizeAlignedWithSegmentSize", 45 plaintextSize: 100, 46 nonceSize: 10, 47 noncePrefixSize: 5, 48 plaintextSegmentSize: 20, 49 firstCiphertextSegmentOffset: 10, 50 chunkSize: 5, 51 }, 52 { 53 name: "plaintextSizeNotAlignedWithSegmentSize", 54 plaintextSize: 110, 55 nonceSize: 10, 56 noncePrefixSize: 5, 57 plaintextSegmentSize: 20, 58 firstCiphertextSegmentOffset: 10, 59 chunkSize: 5, 60 }, 61 { 62 name: "singleSegment", 63 plaintextSize: 100, 64 nonceSize: 10, 65 noncePrefixSize: 5, 66 plaintextSegmentSize: 100, 67 firstCiphertextSegmentOffset: 10, 68 chunkSize: 5, 69 }, 70 { 71 name: "shortPlaintext", 72 plaintextSize: 1, 73 nonceSize: 10, 74 noncePrefixSize: 5, 75 plaintextSegmentSize: 100, 76 firstCiphertextSegmentOffset: 10, 77 chunkSize: 5, 78 }, 79 { 80 name: "shortSegmentSize", 81 plaintextSize: 100, 82 nonceSize: 10, 83 noncePrefixSize: 5, 84 plaintextSegmentSize: 10, 85 firstCiphertextSegmentOffset: 10, 86 chunkSize: 5, 87 }, 88 { 89 name: "largeChunkSize", 90 plaintextSize: 100, 91 nonceSize: 10, 92 noncePrefixSize: 5, 93 plaintextSegmentSize: 10, 94 firstCiphertextSegmentOffset: 10, 95 chunkSize: 500, 96 }, 97 } 98 99 for _, tc := range testcases { 100 t.Run(tc.name, func(t *testing.T) { 101 writerParams := noncebased.WriterParams{ 102 NonceSize: tc.nonceSize, 103 PlaintextSegmentSize: tc.plaintextSegmentSize, 104 FirstCiphertextSegmentOffset: tc.firstCiphertextSegmentOffset, 105 } 106 plaintext, ciphertext, noncePrefix, err := testEncrypt(tc.plaintextSize, tc.noncePrefixSize, writerParams) 107 if err != nil { 108 t.Fatalf("encrypting failed: %v\n", err) 109 } 110 111 readerParams := noncebased.ReaderParams{ 112 NonceSize: tc.nonceSize, 113 NoncePrefix: noncePrefix, 114 CiphertextSegmentSize: tc.plaintextSegmentSize + tc.nonceSize, 115 FirstCiphertextSegmentOffset: tc.firstCiphertextSegmentOffset, 116 } 117 if err := testDecrypt(plaintext, ciphertext, tc.chunkSize, readerParams); err != nil { 118 t.Fatalf("decrypting failed: %v\n", err) 119 } 120 }) 121 } 122} 123func TestNonceBased_invalidParameters(t *testing.T) { 124 125 testcases := []struct { 126 name string 127 plaintextSize int 128 nonceSize int 129 noncePrefixSize int 130 plaintextSegmentSize int 131 firstCiphertextSegmentOffset int 132 chunkSize int 133 expectedError error 134 }{ 135 { 136 name: "nonceTooSmall", 137 plaintextSize: 100, 138 nonceSize: 5, 139 noncePrefixSize: 5, 140 plaintextSegmentSize: 20, 141 firstCiphertextSegmentOffset: 10, 142 chunkSize: 5, 143 expectedError: noncebased.ErrNonceSizeTooShort, 144 }, 145 } 146 147 for _, tc := range testcases { 148 t.Run(tc.name, func(t *testing.T) { 149 writerParams := noncebased.WriterParams{ 150 NonceSize: tc.nonceSize, 151 FirstCiphertextSegmentOffset: tc.firstCiphertextSegmentOffset, 152 } 153 _, _, _, err := testEncrypt(tc.plaintextSize, tc.noncePrefixSize, writerParams) 154 if err != tc.expectedError { 155 t.Errorf("did not produce expected error: got: %q, want: %q\n", err, tc.expectedError) 156 } 157 158 // Prepare empty input for testDecrypt(). 159 ciphertextSegmentSize := tc.plaintextSegmentSize + tc.nonceSize 160 161 ciphertextSize := tc.firstCiphertextSegmentOffset 162 ciphertextSize += (tc.plaintextSize / tc.plaintextSegmentSize) * ciphertextSegmentSize 163 plaintextRemainder := tc.plaintextSize % tc.plaintextSegmentSize 164 if plaintextRemainder > 0 { 165 ciphertextSize += plaintextRemainder + tc.nonceSize 166 } 167 168 readerParams := noncebased.ReaderParams{ 169 NonceSize: tc.nonceSize, 170 NoncePrefix: make([]byte, tc.noncePrefixSize), 171 CiphertextSegmentSize: tc.plaintextSegmentSize + tc.nonceSize, 172 FirstCiphertextSegmentOffset: tc.firstCiphertextSegmentOffset, 173 } 174 if err := testDecrypt(make([]byte, tc.plaintextSize), make([]byte, ciphertextSize), tc.chunkSize, readerParams); err != tc.expectedError { 175 t.Errorf("did not produce expected error: got: %q, want: %q\n", err, tc.expectedError) 176 } 177 }) 178 } 179} 180 181// testEncrypter is essentially a no-op cipher. 182// 183// It produces ciphertexts which contain the plaintext broken into segments, 184// with the unmodified per-segment nonce placed at the end of each segment. 185type testEncrypter struct { 186 noncebased.SegmentEncrypter 187} 188 189func (e testEncrypter) EncryptSegment(segment, nonce []byte) ([]byte, error) { 190 ctLen := len(segment) + len(nonce) 191 ciphertext := make([]byte, ctLen) 192 copy(ciphertext, segment) 193 copy(ciphertext[len(segment):], nonce) 194 return ciphertext, nil 195} 196 197type testDecrypter struct { 198 noncebased.SegmentDecrypter 199} 200 201func (d testDecrypter) DecryptSegment(segment, nonce []byte) ([]byte, error) { 202 tagStart := len(segment) - len(nonce) 203 if tagStart < 0 { 204 return nil, errors.New("segment too short") 205 } 206 tag := segment[tagStart:] 207 if !bytes.Equal(nonce, tag) { 208 return nil, fmt.Errorf("tag mismtach:\nsegment: %s\nnonce: %s\ntag: %s", hex.EncodeToString(segment), hex.EncodeToString(nonce), hex.EncodeToString(tag)) 209 } 210 result := make([]byte, tagStart) 211 copy(result, segment[:tagStart]) 212 return result, nil 213} 214 215// testEncrypt generates a random plaintext and random noncePrefix, then uses 216// them to instantiate a noncebased.Writer and uses it to produce a ciphertext. 217// 218// The plaintext, ciphertext and nonce prefix are returned. 219func testEncrypt(plaintextSize, noncePrefixSize int, wp noncebased.WriterParams) ([]byte, []byte, []byte, error) { 220 var dst bytes.Buffer 221 dstWriter := bufio.NewWriter(&dst) 222 223 noncePrefix := make([]byte, noncePrefixSize) 224 if _, err := rand.Read(noncePrefix); err != nil { 225 return nil, nil, nil, err 226 } 227 228 wp.W = dstWriter 229 wp.SegmentEncrypter = testEncrypter{} 230 wp.NoncePrefix = noncePrefix 231 232 w, err := noncebased.NewWriter(wp) 233 if err != nil { 234 return nil, nil, nil, err 235 } 236 237 plaintext := make([]byte, plaintextSize) 238 if _, err := rand.Read(plaintext); err != nil { 239 return nil, nil, nil, err 240 } 241 242 w.Write(plaintext) 243 w.Close() 244 dstWriter.Flush() 245 ciphertext := dst.Bytes() 246 247 return plaintext, ciphertext, noncePrefix, nil 248} 249 250// testDecrypt instantiates a noncebased.Reader, uses it to decrypt ciphertext 251// and verifies it matches plaintext. While decrypting, it reads in chunkSize 252// increments. 253func testDecrypt(plaintext, ciphertext []byte, chunkSize int, rp noncebased.ReaderParams) error { 254 rp.R = bytes.NewReader(ciphertext) 255 rp.SegmentDecrypter = testDecrypter{} 256 r, err := noncebased.NewReader(rp) 257 if err != nil { 258 return err 259 } 260 261 var ( 262 chunk = make([]byte, chunkSize) 263 decrypted = 0 264 eof = false 265 ) 266 for !eof { 267 n, err := r.Read(chunk) 268 if err != nil && err != io.EOF { 269 return fmt.Errorf("error reading chunk: %v", err) 270 } 271 eof = err == io.EOF 272 got := chunk[:n] 273 want := plaintext[decrypted : decrypted+n] 274 if !bytes.Equal(got, want) { 275 return fmt.Errorf("decrypted data does not match. Got=%s;want=%s", hex.EncodeToString(got), hex.EncodeToString(want)) 276 } 277 decrypted += n 278 } 279 if decrypted != len(plaintext) { 280 return fmt.Errorf("number of decrypted bytes does not match. Got=%d,want=%d", decrypted, len(plaintext)) 281 } 282 return nil 283} 284