mirror of
https://github.com/luxfi/pqsafe.git
synced 2026-10-30 15:21:41 +00:00
135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package capsule implements the PQSAFE recipient capsule: a per-
|
|
// recipient wrap of the per-file DEK under the recipient's X-Wing
|
|
// public key. X-Wing primitives are consumed from luxfi/zwing — the
|
|
// single canonical Lux implementation of the IETF
|
|
// draft-connolly-cfrg-xwing-kem construction.
|
|
//
|
|
// Wire format:
|
|
//
|
|
// capsule := XWingCiphertext (zwing.XWingCiphertextSize bytes) ||
|
|
// AEAD-nonce (12 bytes random per capsule) ||
|
|
// AEAD(DEK; key=HKDF(ss, "PQSAFE-DEK-WRAP-v1"); aad=manifest_hash)
|
|
//
|
|
// The capsule binds to the manifest hash through AEAD AAD: a capsule
|
|
// from one manifest cannot be replayed into another manifest.
|
|
package capsule
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha512"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
"golang.org/x/crypto/hkdf"
|
|
|
|
"github.com/luxfi/zwing"
|
|
|
|
"github.com/luxfi/pqsafe/pkg/ctx"
|
|
)
|
|
|
|
const (
|
|
// WrapNonceSize is the AEAD nonce for the DEK wrap.
|
|
WrapNonceSize = chacha20poly1305.NonceSize // 12
|
|
// WrapAEADKeySize is the HKDF-derived wrap-key size.
|
|
WrapAEADKeySize = 32
|
|
// DEKSize is the file DEK length.
|
|
DEKSize = 32
|
|
)
|
|
|
|
// Errors.
|
|
var (
|
|
ErrBadDEKSize = errors.New("capsule: DEK must be 32 bytes")
|
|
ErrUnwrap = errors.New("capsule: unwrap failed (wrong recipient or tampered)")
|
|
ErrBadCapsule = errors.New("capsule: malformed capsule bytes")
|
|
)
|
|
|
|
// Capsule is the on-wire form of a single recipient's wrapped DEK.
|
|
type Capsule struct {
|
|
KEMCT []byte // zwing.XWingCiphertextSize bytes
|
|
Nonce [WrapNonceSize]byte
|
|
Wrap []byte // AEAD(DEK; aad=manifest_hash) — 32 + 16 = 48 bytes
|
|
}
|
|
|
|
// MarshalBinary serializes a capsule.
|
|
func (c *Capsule) MarshalBinary() []byte {
|
|
out := make([]byte, 0, len(c.KEMCT)+WrapNonceSize+len(c.Wrap))
|
|
out = append(out, c.KEMCT...)
|
|
out = append(out, c.Nonce[:]...)
|
|
out = append(out, c.Wrap...)
|
|
return out
|
|
}
|
|
|
|
// UnmarshalBinary parses a capsule from bytes.
|
|
func UnmarshalBinary(b []byte) (*Capsule, error) {
|
|
if len(b) < zwing.XWingCiphertextSize+WrapNonceSize+DEKSize+16 {
|
|
return nil, ErrBadCapsule
|
|
}
|
|
c := &Capsule{KEMCT: make([]byte, zwing.XWingCiphertextSize)}
|
|
copy(c.KEMCT, b[:zwing.XWingCiphertextSize])
|
|
copy(c.Nonce[:], b[zwing.XWingCiphertextSize:zwing.XWingCiphertextSize+WrapNonceSize])
|
|
c.Wrap = append([]byte(nil), b[zwing.XWingCiphertextSize+WrapNonceSize:]...)
|
|
return c, nil
|
|
}
|
|
|
|
// Seal wraps a file DEK to one recipient.
|
|
func Seal(recipient *zwing.XWingPublicKey, dek, manifestHash []byte) (*Capsule, error) {
|
|
if len(dek) != DEKSize {
|
|
return nil, ErrBadDEKSize
|
|
}
|
|
|
|
kemCT, ss, err := zwing.XWingEncapsulate(rand.Reader, recipient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wrapKey := make([]byte, WrapAEADKeySize)
|
|
kdf := hkdf.New(sha512.New384, ss[:], nil, []byte(ctx.DEKWrap))
|
|
if _, err := kdf.Read(wrapKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aead, err := chacha20poly1305.New(wrapKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &Capsule{KEMCT: kemCT}
|
|
if _, err := rand.Read(c.Nonce[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
c.Wrap = aead.Seal(nil, c.Nonce[:], dek, manifestHash)
|
|
return c, nil
|
|
}
|
|
|
|
// Open unwraps the DEK using the recipient's secret key. The
|
|
// manifest_hash MUST match the bytes used in Seal.
|
|
func (c *Capsule) Open(recipientSec *zwing.XWingPrivateKey, manifestHash []byte) ([]byte, error) {
|
|
ss, err := zwing.XWingDecapsulate(recipientSec, c.KEMCT)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capsule: zwing.XWingDecapsulate: %w", err)
|
|
}
|
|
|
|
wrapKey := make([]byte, WrapAEADKeySize)
|
|
kdf := hkdf.New(sha512.New384, ss[:], nil, []byte(ctx.DEKWrap))
|
|
if _, err := kdf.Read(wrapKey); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
aead, err := chacha20poly1305.New(wrapKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dek, err := aead.Open(nil, c.Nonce[:], c.Wrap, manifestHash)
|
|
if err != nil {
|
|
return nil, ErrUnwrap
|
|
}
|
|
if len(dek) != DEKSize {
|
|
return nil, ErrUnwrap
|
|
}
|
|
return dek, nil
|
|
}
|