Skip to content

Commit f916d93

Browse files
FiloSottilegopherbot
authored andcommitted
crypto/cipher: add NewGCMWithRandomNonce
Fixes #69981 Change-Id: I0cad11f5d7673304c5a6d85fc598ddc27ab93738 Reviewed-on: https://go-review.googlesource.com/c/go/+/629175 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 3809035 commit f916d93

File tree

5 files changed

+151
-5
lines changed

5 files changed

+151
-5
lines changed

api/next/69981.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg crypto/cipher, func NewGCMWithRandomNonce(Block) (AEAD, error) #69981
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The new [NewGCMWithRandomNonce] function returns an [AEAD] that implements
2+
AES-GCM by generating a random nonce during Seal and prepending it to the
3+
ciphertext.

src/crypto/cipher/gcm.go

+117
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,123 @@ func newGCM(cipher Block, nonceSize, tagSize int) (AEAD, error) {
6767
return g, nil
6868
}
6969

70+
// NewGCMWithRandomNonce returns the given cipher wrapped in Galois Counter
71+
// Mode, with randomly-generated nonces. The cipher must have been created by
72+
// [aes.NewCipher].
73+
//
74+
// It generates a random 96-bit nonce, which is prepended to the ciphertext by Seal,
75+
// and is extracted from the ciphertext by Open. The NonceSize of the AEAD is zero,
76+
// while the Overhead is 28 bytes (the combination of nonce size and tag size).
77+
//
78+
// A given key MUST NOT be used to encrypt more than 2^32 messages, to limit the
79+
// risk of a random nonce collision to negligible levels.
80+
func NewGCMWithRandomNonce(cipher Block) (AEAD, error) {
81+
c, ok := cipher.(*aes.Block)
82+
if !ok {
83+
return nil, errors.New("cipher: NewGCMWithRandomNonce requires aes.Block")
84+
}
85+
g, err := gcm.New(c, gcmStandardNonceSize, gcmTagSize)
86+
if err != nil {
87+
return nil, err
88+
}
89+
return gcmWithRandomNonce{g}, nil
90+
}
91+
92+
type gcmWithRandomNonce struct {
93+
*gcm.GCM
94+
}
95+
96+
func (g gcmWithRandomNonce) NonceSize() int {
97+
return 0
98+
}
99+
100+
func (g gcmWithRandomNonce) Overhead() int {
101+
return gcmStandardNonceSize + gcmTagSize
102+
}
103+
104+
func (g gcmWithRandomNonce) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
105+
if len(nonce) != 0 {
106+
panic("crypto/cipher: non-empty nonce passed to GCMWithRandomNonce")
107+
}
108+
109+
ret, out := sliceForAppend(dst, gcmStandardNonceSize+len(plaintext)+gcmTagSize)
110+
if alias.InexactOverlap(out, plaintext) {
111+
panic("crypto/cipher: invalid buffer overlap of output and input")
112+
}
113+
if alias.AnyOverlap(out, additionalData) {
114+
panic("crypto/cipher: invalid buffer overlap of output and additional data")
115+
}
116+
nonce = out[:gcmStandardNonceSize]
117+
ciphertext := out[gcmStandardNonceSize:]
118+
119+
// The AEAD interface allows using plaintext[:0] or ciphertext[:0] as dst.
120+
//
121+
// This is kind of a problem when trying to prepend or trim a nonce, because the
122+
// actual AES-GCTR blocks end up overlapping but not exactly.
123+
//
124+
// In Open, we write the output *before* the input, so unless we do something
125+
// weird like working through a chunk of block backwards, it works out.
126+
//
127+
// In Seal, we could work through the input backwards or intentionally load
128+
// ahead before writing.
129+
//
130+
// However, the crypto/internal/fips/aes/gcm APIs also check for exact overlap,
131+
// so for now we just do a memmove if we detect overlap.
132+
//
133+
// ┌───────────────────────────┬ ─ ─
134+
// │PPPPPPPPPPPPPPPPPPPPPPPPPPP│ │
135+
// └▽─────────────────────────▲┴ ─ ─
136+
// ╲ Seal ╲
137+
// ╲ Open ╲
138+
// ┌───▼─────────────────────────△──┐
139+
// │NN|CCCCCCCCCCCCCCCCCCCCCCCCCCC|T│
140+
// └────────────────────────────────┘
141+
//
142+
if alias.AnyOverlap(out, plaintext) {
143+
copy(ciphertext, plaintext)
144+
plaintext = ciphertext[:len(plaintext)]
145+
}
146+
147+
gcm.SealWithRandomNonce(g.GCM, nonce, ciphertext, plaintext, additionalData)
148+
return ret
149+
}
150+
151+
func (g gcmWithRandomNonce) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
152+
if len(nonce) != 0 {
153+
panic("crypto/cipher: non-empty nonce passed to GCMWithRandomNonce")
154+
}
155+
if len(ciphertext) < gcmStandardNonceSize+gcmTagSize {
156+
return nil, errOpen
157+
}
158+
159+
ret, out := sliceForAppend(dst, len(ciphertext)-gcmStandardNonceSize-gcmTagSize)
160+
if alias.InexactOverlap(out, ciphertext) {
161+
panic("crypto/cipher: invalid buffer overlap of output and input")
162+
}
163+
if alias.AnyOverlap(out, additionalData) {
164+
panic("crypto/cipher: invalid buffer overlap of output and additional data")
165+
}
166+
// See the discussion in Seal. Note that if there is any overlap at this
167+
// point, it's because out = ciphertext, so out must have enough capacity
168+
// even if we sliced the tag off. Also note how [AEAD] specifies that "the
169+
// contents of dst, up to its capacity, may be overwritten".
170+
if alias.AnyOverlap(out, ciphertext) {
171+
nonce = make([]byte, gcmStandardNonceSize)
172+
copy(nonce, ciphertext)
173+
copy(out[:len(ciphertext)], ciphertext[gcmStandardNonceSize:])
174+
ciphertext = out[:len(ciphertext)-gcmStandardNonceSize]
175+
} else {
176+
nonce = ciphertext[:gcmStandardNonceSize]
177+
ciphertext = ciphertext[gcmStandardNonceSize:]
178+
}
179+
180+
_, err := g.GCM.Open(out[:0], nonce, ciphertext, additionalData)
181+
if err != nil {
182+
return nil, err
183+
}
184+
return ret, nil
185+
}
186+
70187
// gcmAble is an interface implemented by ciphers that have a specific optimized
71188
// implementation of GCM. crypto/aes doesn't use this anymore, and we'd like to
72189
// eventually remove it.

src/crypto/cipher/gcm_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"crypto/aes"
1010
"crypto/cipher"
11+
"crypto/internal/boring"
1112
"crypto/internal/cryptotest"
1213
"crypto/internal/fips"
1314
fipsaes "crypto/internal/fips/aes"
@@ -723,6 +724,14 @@ func testGCMAEAD(t *testing.T, newCipher func(key []byte) cipher.Block) {
723724
cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithNonceSize(block, nonceSize) })
724725
})
725726
}
727+
728+
// Test NewGCMWithRandomNonce.
729+
t.Run("GCMWithRandomNonce", func(t *testing.T) {
730+
if _, ok := block.(*wrapper); ok || boring.Enabled {
731+
t.Skip("NewGCMWithRandomNonce requires an AES block cipher")
732+
}
733+
cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithRandomNonce(block) })
734+
})
726735
})
727736
}
728737
}

src/crypto/internal/cryptotest/aead.go

+21-5
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,12 @@ func TestAEAD(t *testing.T, mAEAD MakeAEAD) {
208208
t.Errorf("Seal alters dst instead of appending; got %s, want %s", truncateHex(out[:len(prefix)]), truncateHex(prefix))
209209
}
210210

211-
ciphertext := out[len(prefix):]
212-
// Check that the appended ciphertext wasn't affected by the prefix
213-
if expectedCT := sealMsg(t, aead, nil, nonce, plaintext, addData); !bytes.Equal(ciphertext, expectedCT) {
214-
t.Errorf("Seal behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(ciphertext), truncateHex(expectedCT))
211+
if isDeterministic(aead) {
212+
ciphertext := out[len(prefix):]
213+
// Check that the appended ciphertext wasn't affected by the prefix
214+
if expectedCT := sealMsg(t, aead, nil, nonce, plaintext, addData); !bytes.Equal(ciphertext, expectedCT) {
215+
t.Errorf("Seal behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(ciphertext), truncateHex(expectedCT))
216+
}
215217
}
216218
}
217219
})
@@ -254,7 +256,9 @@ func TestAEAD(t *testing.T, mAEAD MakeAEAD) {
254256
})
255257

256258
t.Run("WrongNonce", func(t *testing.T) {
257-
259+
if aead.NonceSize() == 0 {
260+
t.Skip("AEAD does not use a nonce")
261+
}
258262
// Test all combinations of plaintext and additional data lengths.
259263
for _, ptLen := range lengths {
260264
for _, adLen := range lengths {
@@ -372,6 +376,18 @@ func sealMsg(t *testing.T, aead cipher.AEAD, ciphertext, nonce, plaintext, addDa
372376
return ciphertext
373377
}
374378

379+
func isDeterministic(aead cipher.AEAD) bool {
380+
// Check if the AEAD is deterministic by checking if the same plaintext
381+
// encrypted with the same nonce and additional data produces the same
382+
// ciphertext.
383+
nonce := make([]byte, aead.NonceSize())
384+
addData := []byte("additional data")
385+
plaintext := []byte("plaintext")
386+
ciphertext1 := aead.Seal(nil, nonce, plaintext, addData)
387+
ciphertext2 := aead.Seal(nil, nonce, plaintext, addData)
388+
return bytes.Equal(ciphertext1, ciphertext2)
389+
}
390+
375391
// Helper function to Open and authenticate ciphertext. Checks that Open
376392
// doesn't error (assuming ciphertext was well-formed with corresponding nonce
377393
// and additional data).

0 commit comments

Comments
 (0)