22// Use of this source code is governed by a BSD-style
33// license that can be found in the LICENSE file.
44
5+ // Package tag implements tagged P-256 or hybrid P-256 + ML-KEM-768 recipients,
6+ // which can be used with identities stored on hardware keys, usually supported
7+ // by dedicated plugins.
8+ //
9+ // The tag reduces privacy, by allowing an observer to correlate files with a
10+ // recipient (but not files amongst them without knowledge of the recipient),
11+ // but this is also a desirable property for hardware keys that require user
12+ // interaction for each decryption operation.
513package tag
614
715import (
@@ -18,12 +26,9 @@ import (
1826 "filippo.io/nistec"
1927)
2028
29+ // Recipient is a tagged P-256 or hybrid P-256 + ML-KEM-768 recipient.
2130type Recipient struct {
22- kem hpke.KEMSender
23-
24- mlkem * mlkem.EncapsulationKey768
25- compressed [compressedPointSize ]byte
26- uncompressed [uncompressedPointSize ]byte
31+ kem hpke.PublicKey
2732}
2833
2934var _ age.Recipient = & Recipient {}
@@ -54,9 +59,8 @@ func ParseRecipient(s string) (*Recipient, error) {
5459}
5560
5661const compressedPointSize = 1 + 32
57- const uncompressedPointSize = 1 + 32 + 32
5862
59- // NewRecipient returns a new [Recipient] from a raw public key.
63+ // NewRecipient returns a new P-256 [Recipient] from a raw public key.
6064func NewRecipient (publicKey []byte ) (* Recipient , error ) {
6165 if len (publicKey ) != compressedPointSize {
6266 return nil , fmt .Errorf ("invalid tag recipient public key size %d" , len (publicKey ))
@@ -65,58 +69,30 @@ func NewRecipient(publicKey []byte) (*Recipient, error) {
6569 if err != nil {
6670 return nil , fmt .Errorf ("invalid tag recipient public key: %v" , err )
6771 }
68- k , err := ecdh .P256 ().NewPublicKey (p .Bytes ())
72+ k , err := hpke . DHKEM ( ecdh .P256 () ).NewPublicKey (p .Bytes ())
6973 if err != nil {
7074 return nil , fmt .Errorf ("invalid tag recipient public key: %v" , err )
7175 }
72- kem , err := hpke .DHKEMSender (k )
73- if err != nil {
74- return nil , fmt .Errorf ("failed to create DHKEM sender: %v" , err )
75- }
76- r := & Recipient {kem : kem }
77- copy (r .compressed [:], publicKey )
78- copy (r .uncompressed [:], p .Bytes ())
79- return r , nil
76+ return & Recipient {k }, nil
8077}
8178
82- // NewHybridRecipient returns a new [Recipient] from raw concatenated public keys.
79+ // NewHybridRecipient returns a new hybrid P-256 + ML-KEM-768 [Recipient] from
80+ // raw concatenated public keys.
8381func NewHybridRecipient (publicKey []byte ) (* Recipient , error ) {
84- if len (publicKey ) != compressedPointSize + mlkem .EncapsulationKeySize768 {
85- return nil , fmt .Errorf ("invalid tagpq recipient public key size %d" , len (publicKey ))
86- }
87- p , err := nistec .NewP256Point ().SetBytes (publicKey )
88- if err != nil {
89- return nil , fmt .Errorf ("invalid tagpq recipient DH public key: %v" , err )
90- }
91- k , err := ecdh .P256 ().NewPublicKey (p .Bytes ())
82+ k , err := hpke .MLKEM768P256 ().NewPublicKey (publicKey )
9283 if err != nil {
93- return nil , fmt .Errorf ("invalid tagpq recipient DH public key: %v" , err )
84+ return nil , fmt .Errorf ("invalid tagpq recipient public key: %v" , err )
9485 }
95- pq , err := mlkem .NewEncapsulationKey768 (publicKey [compressedPointSize :])
96- if err != nil {
97- return nil , fmt .Errorf ("invalid tagpq recipient PQ public key: %v" , err )
98- }
99- kem , err := hpke .QSFSender (k , pq )
100- if err != nil {
101- return nil , fmt .Errorf ("failed to create DHKEM sender: %v" , err )
102- }
103- r := & Recipient {kem : kem , mlkem : pq }
104- copy (r .compressed [:], publicKey [:compressedPointSize ])
105- copy (r .uncompressed [:], p .Bytes ())
106- return r , nil
86+ return & Recipient {k }, nil
10787}
10888
109- var p256TagLabel = []byte ("age-encryption.org/p256tag" )
110- var p256MLKEM768TagLabel = []byte ("age-encryption.org/p256mlkem768tag" )
111-
11289func (r * Recipient ) Wrap (fileKey []byte ) ([]* age.Stanza , error ) {
113- label , arg := p256TagLabel , "p256tag"
114- if r .mlkem != nil {
115- label , arg = p256MLKEM768TagLabel , "p256mlkem768tag"
90+ label , arg := "age-encryption.org/p256tag" , "p256tag"
91+ if r .kem . KEM (). ID () == hpke . MLKEM768P256 (). ID () {
92+ label , arg = "age-encryption.org/mlkem768p256tag" , "p256mlkem768tag"
11693 }
11794
118- enc , s , err := hpke .NewSender (r .kem ,
119- hpke .HKDFSHA256 (), hpke .ChaCha20Poly1305 (), label )
95+ enc , s , err := hpke .NewSender (r .kem , hpke .HKDFSHA256 (), hpke .ChaCha20Poly1305 (), []byte (label ))
12096 if err != nil {
12197 return nil , fmt .Errorf ("failed to set up HPKE sender: %v" , err )
12298 }
@@ -125,8 +101,13 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
125101 return nil , fmt .Errorf ("failed to encrypt file key: %v" , err )
126102 }
127103
128- tag , err := hkdf .Extract (sha256 .New ,
129- append (enc [:uncompressedPointSize ], r .uncompressed [:]... ), label )
104+ tagEnc , tagRecipient := enc , r .kem .Bytes ()
105+ if r .kem .KEM ().ID () == hpke .MLKEM768P256 ().ID () {
106+ // In hybrid mode, the tag is computed over just the P-256 part.
107+ tagEnc = enc [mlkem .CiphertextSize768 :]
108+ tagRecipient = tagRecipient [mlkem .EncapsulationKeySize768 :]
109+ }
110+ tag , err := hkdf .Extract (sha256 .New , append (tagEnc , tagRecipient ... ), []byte (label ))
130111 if err != nil {
131112 return nil , fmt .Errorf ("failed to compute tag: %v" , err )
132113 }
@@ -145,8 +126,8 @@ func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
145126
146127// String returns the Bech32 public key encoding of r.
147128func (r * Recipient ) String () string {
148- if r .mlkem != nil {
149- return plugin .EncodeRecipient ("tagpq" , append ( r . compressed [:], r . mlkem . Bytes () ... ))
129+ if r .kem . KEM (). ID () == hpke . MLKEM768P256 (). ID () {
130+ return plugin .EncodeRecipient ("tagpq" , r . kem . Bytes ())
150131 }
151- return plugin .EncodeRecipient ("tag" , r .compressed [:] )
132+ return plugin .EncodeRecipient ("tag" , r .kem . Bytes () )
152133}
0 commit comments