// Copyright 2024 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package hpke implements Hybrid Public Key Encryption (HPKE) as defined in // [RFC 9180]. // // [RFC 9180]: https://www.rfc-editor.org/rfc/rfc9180.html package hpke import ( "crypto/cipher" "errors" "internal/byteorder" ) type context struct { suiteID []byte export func(string, uint16) ([]byte, error) aead cipher.AEAD baseNonce []byte // seqNum starts at zero and is incremented for each Seal/Open call. // 64 bits are enough not to overflow for 500 years at 1ns per operation. seqNum uint64 } // Sender is a sending HPKE context. It is instantiated with a specific KEM // encapsulation key (i.e. the public key), and it is stateful, incrementing the // nonce counter for each [Sender.Seal] call. type Sender struct { *context } // Recipient is a receiving HPKE context. It is instantiated with a specific KEM // decapsulation key (i.e. the secret key), and it is stateful, incrementing the // nonce counter for each successful [Recipient.Open] call. type Recipient struct { *context } func newContext(sharedSecret []byte, kemID uint16, kdf KDF, aead AEAD, info []byte) (*context, error) { sid := suiteID(kemID, kdf.ID(), aead.ID()) if kdf.oneStage() { secrets := make([]byte, 0, 2+2+len(sharedSecret)) secrets = byteorder.BEAppendUint16(secrets, 0) // empty psk secrets = byteorder.BEAppendUint16(secrets, uint16(len(sharedSecret))) secrets = append(secrets, sharedSecret...) ksContext := make([]byte, 0, 1+2+2+len(info)) ksContext = append(ksContext, 0) // mode 0 ksContext = byteorder.BEAppendUint16(ksContext, 0) // empty psk_id ksContext = byteorder.BEAppendUint16(ksContext, uint16(len(info))) ksContext = append(ksContext, info...) secret, err := kdf.labeledDerive(sid, secrets, "secret", ksContext, uint16(aead.keySize()+aead.nonceSize()+kdf.size())) if err != nil { return nil, err } key := secret[:aead.keySize()] baseNonce := secret[aead.keySize() : aead.keySize()+aead.nonceSize()] expSecret := secret[aead.keySize()+aead.nonceSize():] a, err := aead.aead(key) if err != nil { return nil, err } export := func(exporterContext string, length uint16) ([]byte, error) { return kdf.labeledDerive(sid, expSecret, "sec", []byte(exporterContext), length) } return &context{ aead: a, suiteID: sid, export: export, baseNonce: baseNonce, }, nil } pskIDHash, err := kdf.labeledExtract(sid, nil, "psk_id_hash", nil) if err != nil { return nil, err } infoHash, err := kdf.labeledExtract(sid, nil, "info_hash", info) if err != nil { return nil, err } ksContext := append([]byte{0}, pskIDHash...) ksContext = append(ksContext, infoHash...) secret, err := kdf.labeledExtract(sid, sharedSecret, "secret", nil) if err != nil { return nil, err } key, err := kdf.labeledExpand(sid, secret, "key", ksContext, uint16(aead.keySize())) if err != nil { return nil, err } a, err := aead.aead(key) if err != nil { return nil, err } baseNonce, err := kdf.labeledExpand(sid, secret, "base_nonce", ksContext, uint16(aead.nonceSize())) if err != nil { return nil, err } expSecret, err := kdf.labeledExpand(sid, secret, "exp", ksContext, uint16(kdf.size())) if err != nil { return nil, err } export := func(exporterContext string, length uint16) ([]byte, error) { return kdf.labeledExpand(sid, expSecret, "sec", []byte(exporterContext), length) } return &context{ aead: a, suiteID: sid, export: export, baseNonce: baseNonce, }, nil } // NewSender returns a sending HPKE context for the provided KEM encapsulation // key (i.e. the public key), and using the ciphersuite defined by the // combination of KEM, KDF, and AEAD. // // The info parameter is additional public information that must match between // sender and recipient. // // The returned enc ciphertext can be used to instantiate a matching receiving // HPKE context with the corresponding KEM decapsulation key. func NewSender(pk PublicKey, kdf KDF, aead AEAD, info []byte) (enc []byte, s *Sender, err error) { sharedSecret, encapsulatedKey, err := pk.encap() if err != nil { return nil, nil, err } context, err := newContext(sharedSecret, pk.KEM().ID(), kdf, aead, info) if err != nil { return nil, nil, err } return encapsulatedKey, &Sender{context}, nil } // NewRecipient returns a receiving HPKE context for the provided KEM // decapsulation key (i.e. the secret key), and using the ciphersuite defined by // the combination of KEM, KDF, and AEAD. // // The enc parameter must have been produced by a matching sending HPKE context // with the corresponding KEM encapsulation key. The info parameter is // additional public information that must match between sender and recipient. func NewRecipient(enc []byte, k PrivateKey, kdf KDF, aead AEAD, info []byte) (*Recipient, error) { sharedSecret, err := k.decap(enc) if err != nil { return nil, err } context, err := newContext(sharedSecret, k.KEM().ID(), kdf, aead, info) if err != nil { return nil, err } return &Recipient{context}, nil } // Seal encrypts the provided plaintext, optionally binding to the additional // public data aad. // // Seal uses incrementing counters for each call, and Open on the receiving side // must be called in the same order as Seal. func (s *Sender) Seal(aad, plaintext []byte) ([]byte, error) { if s.aead == nil { return nil, errors.New("export-only instantiation") } ciphertext := s.aead.Seal(nil, s.nextNonce(), plaintext, aad) s.seqNum++ return ciphertext, nil } // Seal instantiates a single-use HPKE sending HPKE context like [NewSender], // and then encrypts the provided plaintext like [Sender.Seal] (with no aad). // Seal returns the concatenation of the encapsulated key and the ciphertext. func Seal(pk PublicKey, kdf KDF, aead AEAD, info, plaintext []byte) ([]byte, error) { enc, s, err := NewSender(pk, kdf, aead, info) if err != nil { return nil, err } ct, err := s.Seal(nil, plaintext) if err != nil { return nil, err } return append(enc, ct...), nil } // Export produces a secret value derived from the shared key between sender and // recipient. length must be at most 65,535. func (s *Sender) Export(exporterContext string, length int) ([]byte, error) { if length < 0 || length > 0xFFFF { return nil, errors.New("invalid length") } return s.export(exporterContext, uint16(length)) } // Open decrypts the provided ciphertext, optionally binding to the additional // public data aad, or returns an error if decryption fails. // // Open uses incrementing counters for each successful call, and must be called // in the same order as Seal on the sending side. func (r *Recipient) Open(aad, ciphertext []byte) ([]byte, error) { if r.aead == nil { return nil, errors.New("export-only instantiation") } plaintext, err := r.aead.Open(nil, r.nextNonce(), ciphertext, aad) if err != nil { return nil, err } r.seqNum++ return plaintext, nil } // Open instantiates a single-use HPKE receiving HPKE context like [NewRecipient], // and then decrypts the provided ciphertext like [Recipient.Open] (with no aad). // ciphertext must be the concatenation of the encapsulated key and the actual ciphertext. func Open(k PrivateKey, kdf KDF, aead AEAD, info, ciphertext []byte) ([]byte, error) { encSize := k.KEM().encSize() if len(ciphertext) < encSize { return nil, errors.New("ciphertext too short") } enc, ciphertext := ciphertext[:encSize], ciphertext[encSize:] r, err := NewRecipient(enc, k, kdf, aead, info) if err != nil { return nil, err } return r.Open(nil, ciphertext) } // Export produces a secret value derived from the shared key between sender and // recipient. length must be at most 65,535. func (r *Recipient) Export(exporterContext string, length int) ([]byte, error) { if length < 0 || length > 0xFFFF { return nil, errors.New("invalid length") } return r.export(exporterContext, uint16(length)) } func (ctx *context) nextNonce() []byte { nonce := make([]byte, ctx.aead.NonceSize()) byteorder.BEPutUint64(nonce[len(nonce)-8:], ctx.seqNum) for i := range ctx.baseNonce { nonce[i] ^= ctx.baseNonce[i] } return nonce } func suiteID(kemID, kdfID, aeadID uint16) []byte { suiteID := make([]byte, 0, 4+2+2+2) suiteID = append(suiteID, []byte("HPKE")...) suiteID = byteorder.BEAppendUint16(suiteID, kemID) suiteID = byteorder.BEAppendUint16(suiteID, kdfID) suiteID = byteorder.BEAppendUint16(suiteID, aeadID) return suiteID }