Skip to content

Commit 63f920c

Browse files
committed
feat: Improve API for encryption with a session key
With RFC 9580, clients can encrypt data using AEAD with SEIPDv2 packets. However, SEIPDv2 packets are only compatible with PKESK v6 and SKESK v6 packets when the session key is encrypted using an OpenPGP certificate. This commit improves the API to reduce the risk of encrypting data with SEIPDv2 while using a session key encrypted in a packet version below v6. Specifically, the session key now includes an indicator of whether it is intended for AEAD use or not and affects the produced packets.
1 parent 84a5f30 commit 63f920c

8 files changed

+216
-16
lines changed

crypto/crypto.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ func (p *PGPHandle) LockKey(key *Key, passphrase []byte) (*Key, error) {
7373
}
7474

7575
// GenerateSessionKey generates a random session key for the profile.
76+
// Use GenerateSessionKey on the encryption handle, if the PGP encryption keys are known.
77+
// This function only considers the profile to determine the session key type.
7678
func (p *PGPHandle) GenerateSessionKey() (*SessionKey, error) {
7779
config := p.profile.EncryptionConfig()
78-
return generateSessionKey(config)
80+
return generateSessionKey(config, nil, nil)
7981
}

crypto/decryption_core.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ Loop:
191191
}
192192
}
193193
var dc packet.CipherFunction
194-
if !sessionKey.v6 {
194+
if sessionKey.hasAlgorithm() {
195195
dc, err = sessionKey.GetCipherFunc()
196196
if err != nil {
197197
return nil, errors.Wrap(err, "gopenpgp: unable to decrypt with session key")

crypto/encryption.go

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type PGPEncryption interface {
2323
// EncryptSessionKey encrypts a session key with the encryption handle.
2424
// To encrypt a session key, the handle must contain either recipients or a password.
2525
EncryptSessionKey(sessionKey *SessionKey) ([]byte, error)
26+
// GenerateSessionKey generates a random session key for the given encryption handle
27+
// considering the algorithm preferences of the recipient keys.
28+
GenerateSessionKey() (*SessionKey, error)
2629
// ClearPrivateParams clears all private key material contained in EncryptionHandle from memory.
2730
ClearPrivateParams()
2831
}

crypto/encryption_core.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (eh *encryptionHandle) encryptStreamWithSessionKeyHelper(
216216
return nil, nil, err
217217
}
218218

219-
if !eh.SessionKey.v6 {
219+
if eh.SessionKey.hasAlgorithm() {
220220
config.DefaultCipher, err = eh.SessionKey.GetCipherFunc()
221221
if err != nil {
222222
return nil, nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key")
@@ -226,7 +226,7 @@ func (eh *encryptionHandle) encryptStreamWithSessionKeyHelper(
226226
encryptWriter, err = packet.SerializeSymmetricallyEncrypted(
227227
dataPacketWriter,
228228
config.Cipher(),
229-
config.AEAD() != nil,
229+
eh.SessionKey.v6,
230230
packet.CipherSuite{Cipher: config.Cipher(), Mode: config.AEAD().Mode()},
231231
eh.SessionKey.Key,
232232
config,
@@ -349,7 +349,7 @@ func (eh *encryptionHandle) encryptSignDetachedStreamToRecipients(
349349
configInput.Time = NewConstantClock(eh.clock().Unix())
350350
// Generate a session key for encryption.
351351
if eh.SessionKey == nil {
352-
eh.SessionKey, err = generateSessionKey(configInput)
352+
eh.SessionKey, err = eh.GenerateSessionKey()
353353
if err != nil {
354354
return nil, err
355355
}

crypto/encryption_handle.go

+8
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ func (eh *encryptionHandle) EncryptSessionKey(sessionKey *SessionKey) ([]byte, e
131131
return nil, errors.New("gopenpgp: no password or recipients in encryption handle")
132132
}
133133

134+
// GenerateSessionKey generates a random session key for the given encryption handle
135+
// considering the algorithm preferences of the recipient keys.
136+
func (eh *encryptionHandle) GenerateSessionKey() (*SessionKey, error) {
137+
config := eh.profile.EncryptionConfig()
138+
config.Time = NewConstantClock(eh.clock().Unix())
139+
return generateSessionKey(config, eh.Recipients, eh.HiddenRecipients)
140+
}
141+
134142
// --- Helper methods on encryption handle
135143

136144
func (eh *encryptionHandle) validate() error {

crypto/encryption_session.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ func encryptSessionKeyToWriter(
128128
}
129129
pubKeys = append(pubKeys, encryptionKey.PublicKey)
130130
}
131+
if sk.v6 {
132+
aeadSupport = true
133+
}
131134
if len(pubKeys) == 0 {
132135
return errors.New("gopenpgp: cannot set key: no public key available")
133136
}
@@ -201,10 +204,11 @@ func encryptSessionKeyWithPasswordToWriter(password []byte, sk *SessionKey, outp
201204
cf = config.Cipher()
202205
} else {
203206
cf, err = sk.GetCipherFunc()
207+
if err != nil {
208+
return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
209+
}
204210
}
205-
if err != nil {
206-
return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
207-
}
211+
useAead := sk.v6
208212

209213
if len(password) == 0 {
210214
return errors.New("gopenpgp: password can't be empty")
@@ -215,7 +219,7 @@ func encryptSessionKeyWithPasswordToWriter(password []byte, sk *SessionKey, outp
215219
}
216220
config.DefaultCipher = cf
217221

218-
err = packet.SerializeSymmetricKeyEncryptedReuseKey(outputWriter, sk.Key, password, config)
222+
err = packet.SerializeSymmetricKeyEncryptedAEADReuseKey(outputWriter, sk.Key, password, useAead, config)
219223
if err != nil {
220224
return errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
221225
}

crypto/sessionkey.go

+89-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ type SessionKey struct {
1919
// Algo defines the symmetric encryption algorithm used with this key.
2020
// Only present if the key was not parsed from a v6 packet.
2121
Algo string
22-
// v6 is a flag to indicate that the session key was parsed from a v6 PKESK or SKESK packet
22+
// v6 is a flag to indicate that the session key is capable
23+
// to be used in v6 PKESK or SKESK, and SEIPDv2 packets
2324
v6 bool
2425
}
2526

@@ -66,8 +67,8 @@ func (cr checkReader) Read(buf []byte) (int, error) {
6667
// with this SessionKey.
6768
// Not supported in go-mobile clients use sk.GetCipherFuncInt instead.
6869
func (sk *SessionKey) GetCipherFunc() (packet.CipherFunction, error) {
69-
if sk.v6 {
70-
return 0, errors.New("gopenpgp: no cipher function available for a v6 session key")
70+
if !sk.hasAlgorithm() {
71+
return 0, errors.New("gopenpgp: no cipher function available for the session key")
7172
}
7273
cf, ok := symKeyAlgos[sk.Algo]
7374
if !ok {
@@ -89,6 +90,11 @@ func (sk *SessionKey) GetBase64Key() string {
8990
return base64.StdEncoding.EncodeToString(sk.Key)
9091
}
9192

93+
// IsV6 indicates if the session key can be used with SEIPDv2, PKESKv6/SKESKv6.
94+
func (sk *SessionKey) IsV6() bool {
95+
return sk.v6
96+
}
97+
9298
// RandomToken generates a random token with the specified key size.
9399
func RandomToken(size int) ([]byte, error) {
94100
config := &packet.Config{DefaultCipher: packet.CipherAES256}
@@ -118,13 +124,61 @@ func GenerateSessionKeyAlgo(algo string) (sk *SessionKey, err error) {
118124
return sk, nil
119125
}
120126

121-
// GenerateSessionKey generates a random key for the default cipher.
122-
func generateSessionKey(config *packet.Config) (*SessionKey, error) {
123-
cf, ok := algosToSymKey[config.DefaultCipher]
127+
// GenerateSessionKey generates a random key.
128+
// Considers the cipher and aead preferences in recipients and hiddenRecipients for
129+
// session key generation.
130+
func generateSessionKey(config *packet.Config, recipients *KeyRing, hiddenRecipients *KeyRing) (*SessionKey, error) {
131+
candidateCiphers := []uint8{
132+
uint8(packet.CipherAES256),
133+
uint8(packet.CipherAES128),
134+
}
135+
136+
currentTime := config.Now()
137+
aeadSupport := config.AEADConfig != nil
138+
for _, e := range append(recipients.getEntities(), hiddenRecipients.getEntities()...) {
139+
primarySelfSignature, _ := e.PrimarySelfSignature(currentTime, config)
140+
if primarySelfSignature == nil {
141+
continue
142+
}
143+
144+
if !primarySelfSignature.SEIPDv2 {
145+
aeadSupport = false
146+
}
147+
148+
candidateCiphers = intersectPreferences(candidateCiphers, primarySelfSignature.PreferredSymmetric)
149+
}
150+
151+
if len(candidateCiphers) == 0 {
152+
candidateCiphers = []uint8{uint8(packet.CipherAES128)}
153+
}
154+
cipher := packet.CipherFunction(candidateCiphers[0])
155+
156+
// If the cipher specified by config is a candidate, we'll use that.
157+
configuredCipher := config.Cipher()
158+
for _, c := range candidateCiphers {
159+
cipherFunc := packet.CipherFunction(c)
160+
if cipherFunc == configuredCipher {
161+
cipher = cipherFunc
162+
break
163+
}
164+
}
165+
166+
algo, ok := algosToSymKey[cipher]
124167
if !ok {
125168
return nil, errors.New("gopenpgp: unsupported cipher function")
126169
}
127-
return GenerateSessionKeyAlgo(cf)
170+
171+
r, err := RandomToken(cipher.KeySize())
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
sk := &SessionKey{
177+
Key: r,
178+
Algo: algo,
179+
v6: aeadSupport,
180+
}
181+
return sk, nil
128182
}
129183

130184
// NewSessionKeyFromToken creates a SessionKey struct with the given token and algorithm.
@@ -136,6 +190,16 @@ func NewSessionKeyFromToken(token []byte, algo string) *SessionKey {
136190
}
137191
}
138192

193+
// NewSessionKeyFromTokenWithAead creates a SessionKey struct with the given token and algorithm,
194+
// If aead is set to true, the key is used with v6 PKESK or SKESK, and SEIPDv2 packets.
195+
func NewSessionKeyFromTokenWithAead(token []byte, algo string, aead bool) *SessionKey {
196+
return &SessionKey{
197+
Key: clone(token),
198+
Algo: algo,
199+
v6: aead,
200+
}
201+
}
202+
139203
func newSessionKeyFromEncrypted(ek *packet.EncryptedKey) (*SessionKey, error) {
140204
var algo string
141205
for k, v := range symKeyAlgos {
@@ -178,6 +242,10 @@ func (sk *SessionKey) checkSize() error {
178242
return nil
179243
}
180244

245+
func (sk *SessionKey) hasAlgorithm() bool {
246+
return sk.Algo != ""
247+
}
248+
181249
func getAlgo(cipher packet.CipherFunction) string {
182250
algo := ""
183251
for k, v := range symKeyAlgos {
@@ -188,3 +256,17 @@ func getAlgo(cipher packet.CipherFunction) string {
188256
}
189257
return algo
190258
}
259+
260+
func intersectPreferences(a []uint8, b []uint8) (intersection []uint8) {
261+
var currentIndex int
262+
for _, valueFirst := range a {
263+
for _, valueSecond := range b {
264+
if valueFirst == valueSecond {
265+
a[currentIndex] = valueFirst
266+
currentIndex++
267+
break
268+
}
269+
}
270+
}
271+
return a[:currentIndex]
272+
}

crypto/sessionkey_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package crypto
22

33
import (
4+
"bytes"
45
"encoding/base64"
56
"encoding/hex"
67
"os"
78
"testing"
89

10+
"github.com/ProtonMail/go-crypto/openpgp/packet"
911
"github.com/ProtonMail/gopenpgp/v3/constants"
12+
"github.com/ProtonMail/gopenpgp/v3/profile"
1013
"github.com/stretchr/testify/assert"
1114
)
1215

@@ -373,3 +376,101 @@ func TestAsymmetricKeyPacketDecryptionFailure(t *testing.T) {
373376
_, err = decryptor.DecryptSessionKey(keyPacket)
374377
assert.Error(t, err, "gopenpgp: unable to decrypt session key")
375378
}
379+
380+
func TestSessionKeyAeadHandling(t *testing.T) {
381+
pgp := PGPWithProfile(profile.Default())
382+
profileAead := profile.Default()
383+
profileAead.AeadEncryption = &packet.AEADConfig{}
384+
pgpAead := PGPWithProfile(profileAead)
385+
386+
keyDefault, err := pgp.KeyGeneration().AddUserId("nodeKey", "nodeKey").New().GenerateKey()
387+
if err != nil {
388+
t.Fatal(err)
389+
}
390+
391+
keyAEAD, err := pgpAead.KeyGeneration().AddUserId("nodeKey", "nodeKey").New().GenerateKey()
392+
if err != nil {
393+
t.Fatal(err)
394+
}
395+
396+
encHandle, _ := pgpAead.Encryption().Recipient(keyDefault).New()
397+
encHandleAead, _ := pgpAead.Encryption().Recipient(keyAEAD).New()
398+
399+
sessionKey, err := encHandle.GenerateSessionKey()
400+
if err != nil {
401+
t.Fatal(err)
402+
}
403+
sessionKeyAead, err := encHandleAead.GenerateSessionKey()
404+
if err != nil {
405+
t.Fatal(err)
406+
}
407+
408+
if sessionKey.IsV6() {
409+
t.Error("Expected session key to be non-v6 compatible")
410+
}
411+
if !sessionKeyAead.IsV6() {
412+
t.Error("Expected session key to be v6 compatible")
413+
}
414+
415+
pkeskv3, err := encHandle.EncryptSessionKey(sessionKey)
416+
if err != nil {
417+
t.Fatal(err)
418+
}
419+
420+
pkeskv6, err := encHandle.EncryptSessionKey(sessionKeyAead)
421+
if err != nil {
422+
t.Fatal(err)
423+
}
424+
425+
checkPKESK(t, pkeskv3, 3)
426+
checkPKESK(t, pkeskv6, 6)
427+
428+
encHandle, _ = pgpAead.Encryption().SessionKey(sessionKey).New()
429+
encHandleAead, _ = pgpAead.Encryption().SessionKey(sessionKeyAead).New()
430+
431+
seipdv1, err := encHandle.Encrypt([]byte("hello"))
432+
if err != nil {
433+
t.Fatal(err)
434+
}
435+
seipdv2, err := encHandleAead.Encrypt([]byte("hello"))
436+
if err != nil {
437+
t.Fatal(err)
438+
}
439+
440+
checkSEIPD(t, seipdv1.DataPacket, 1)
441+
checkSEIPD(t, seipdv2.DataPacket, 2)
442+
}
443+
444+
func checkPKESK(t *testing.T, data []byte, version int) {
445+
packets := packet.NewReader(bytes.NewReader(data))
446+
p, err := packets.Next()
447+
if err != nil {
448+
t.Fatal(err)
449+
}
450+
451+
pk, ok := p.(*packet.EncryptedKey)
452+
if !ok {
453+
t.Fatal("Expected PKESK packet")
454+
}
455+
456+
if pk.Version != version {
457+
t.Errorf("Expected PKESK version %d, got %d", version, pk.Version)
458+
}
459+
}
460+
461+
func checkSEIPD(t *testing.T, data []byte, version int) {
462+
packets := packet.NewReader(bytes.NewReader(data))
463+
p, err := packets.Next()
464+
if err != nil {
465+
t.Fatal(err)
466+
}
467+
468+
pk, ok := p.(*packet.SymmetricallyEncrypted)
469+
if !ok {
470+
t.Fatal("Expected SEIPD packet")
471+
}
472+
473+
if pk.Version != version {
474+
t.Errorf("Expected SEIPD version %d, got %d", version, pk.Version)
475+
}
476+
}

0 commit comments

Comments
 (0)