xref: /aosp_15_r20/external/tink/go/streamingaead/subtle/noncebased/noncebased_test.go (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
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