Documentation
¶
Overview ¶
Package fee composes the FEE (Filecoin Encryption Envelope) primitives into a single, small public API for encrypting and decrypting whole objects.
The cryptographic building blocks each live in a sub-package and are deliberately unaware of one another:
- fee/cose — the COSE_Encrypt (tag 96) / COSE_Encrypt0 (tag 16) envelope with a detached payload, and the Enc_structure that authenticates the protected header as AEAD additional data (AAD).
- fee/aesstream — the chunked AES-256-GCM-STREAM body cipher: it seals the plaintext under a per-object content-encryption key (CEK) and a random base nonce.
- fee/ecdhkw — the ECDH-ES+A256KW key wrap over X25519: it encrypts the CEK to a recipient's X25519 public key.
- fee/aeskw — RFC 3394 AES Key Wrap (A256KW): it wraps the CEK directly under a symmetric key-encryption key (KEK).
This package sequences them so callers do not have to. Encrypt generates a fresh CEK, seals the plaintext with the STREAM body cipher, wraps the CEK to each Recipient, and encodes the COSE envelope; Decrypt reverses the process, locating the recipient that a RecipientUnwrapper holds the key for, recovering the CEK, and streaming out the plaintext.
Recipients and content-encryption keys ¶
The body is sealed under a single content-encryption key (CEK). Two concerns are independent: which algorithm wraps the CEK, and how the CEK reaches the decryptor.
A CEK can be carried in the envelope as one or more COSE_Recipient entries (a COSE_Encrypt, tag 96), each keyed by a caller-supplied key id (kid — opaque to this package, e.g. a DID verification method ID). Two wrap algorithms are available and may be mixed in one envelope; on decrypt the caller never selects the algorithm, the recipient's COSE header does:
- ECDH-ES+A256KW to an X25519 public key: NewECDHESRecipient / NewECDHESUnwrapper.
- A256KW under a symmetric KEK: NewA256KWRecipient / NewA256KWUnwrapper.
Alternatively the CEK can be managed out of band — generated or unwrapped by a custody service and handed to this package directly. EncryptWithCEK seals under a caller-provided CEK, and with no recipients it emits a recipient-less COSE_Encrypt0 (tag 16); DecryptWithCEK decrypts with a caller-provided CEK, accepting either tag and ignoring any recipients.
Wire format ¶
The blob is the detached-payload convention: the encoded COSE envelope immediately followed by the STREAM ciphertext (envelope || ciphertext). The body protected header pins the FEE envelope type (EnvelopeType) and the body algorithm (chunked AES-256-GCM-STREAM); the body unprotected header carries the STREAM base nonce in the COSE iv parameter, the plaintext chunk size, and — when the plaintext length is known — the chunk count. These conventions and their private-use label values match the foc-encryption reference and the FEE cross-implementation vectors (see fee/vectors).
Streaming ¶
Both directions stream with O(chunk size) memory. Encrypt returns an io.ReadCloser over envelope||ciphertext, produced as the plaintext is read; Decrypt reads only the (small) envelope header up front and streams the detached ciphertext from its source on demand. Neither buffers the whole object.
Scope ¶
This package covers full-object encrypt/decrypt only. Range-based decryption is a separate primitive in fee/aesstream, keyed off the ciphertext length and chunk size rather than the envelope's chunk count; a higher-level range API is tracked separately. This package adds no cryptography of its own.
Index ¶
- Constants
- Variables
- func Decrypt(src io.Reader, unwrap RecipientUnwrapper) (io.Reader, error)
- func DecryptWithCEK(src io.Reader, cek []byte) (io.Reader, error)
- func Encrypt(plaintext io.Reader, recipients []Recipient, opts ...EncryptOption) (io.ReadCloser, error)
- func EncryptWithCEK(plaintext io.Reader, cek []byte, recipients []Recipient, opts ...EncryptOption) (io.ReadCloser, error)
- type EncryptOption
- type Recipient
- type RecipientUnwrapper
Constants ¶
const EnvelopeType = "application/vnd.foc-envelope+cose"
EnvelopeType is the COSE "typ" (RFC 9596, header label 16) that every FEE envelope is pinned to. Encrypt writes it into the body protected header and Decrypt requires it on decode, so a blob that is not a FEE envelope is rejected before any key material is touched. Its value matches the foc-encryption reference.
Variables ¶
var ( // ErrNoRecipients means Encrypt was called with no recipients. Encrypt // wraps a freshly generated CEK, so it needs at least one recipient to be // recoverable; use EncryptWithCEK for a recipient-less (external-CEK) envelope. ErrNoRecipients = errors.New("fee: at least one recipient is required") // ErrNoMatchingRecipient means no recipient entry in the envelope carries a // kid equal to the unwrapper's key id, so there is no wrapped CEK for this // unwrapper to recover. ErrNoMatchingRecipient = errors.New("fee: no envelope recipient matches the unwrapper's key id") // ErrNoRecipientsInEnvelope means Decrypt was given a recipient-less // COSE_Encrypt0 (tag 16) envelope; recover it with DecryptWithCEK instead. ErrNoRecipientsInEnvelope = errors.New("fee: envelope carries no recipients; use DecryptWithCEK") // ErrUnsupportedBodyAlg means the envelope's body algorithm header is absent // or is not the FEE chunked AES-256-GCM-STREAM cipher. ErrUnsupportedBodyAlg = errors.New("fee: unsupported body algorithm") // ErrUnsupportedRecipientAlg means a matched recipient's key-wrap algorithm // header does not match the unwrapper that was asked to recover it (e.g. an // ECDH-ES unwrapper matched against an A256KW recipient). ErrUnsupportedRecipientAlg = errors.New("fee: unsupported recipient key-wrap algorithm") // ErrMalformedEnvelope means a required body header was missing or had the // wrong type, or a declared parameter was out of range. ErrMalformedEnvelope = errors.New("fee: malformed FEE envelope") // ErrNilUnwrapper means Decrypt was given a nil RecipientUnwrapper. ErrNilUnwrapper = errors.New("fee: nil recipient unwrapper") // ErrInvalidCEK means a caller-provided content-encryption key (see // EncryptWithCEK / DecryptWithCEK) was not the required AES-256 key length. ErrInvalidCEK = errors.New("fee: content-encryption key must be 32 bytes") // ErrContentLengthMismatch means the plaintext length declared via // WithContentLength did not match the number of bytes actually read; it // surfaces from the returned reader, and the envelope's chunk count (already // written) is not to be trusted. ErrContentLengthMismatch = errors.New("fee: plaintext length did not match the declared content length") )
Sentinel errors. Decrypt failures that originate in a sub-package are wrapped rather than replaced, so errors.Is still matches the sub-package sentinel (e.g. cose.ErrMalformed, aeskw.ErrIntegrity, aesstream.ErrCorrupted) in addition to the fee-level classification here.
Functions ¶
func Decrypt ¶
Decrypt recovers the plaintext from a FEE COSE_Encrypt (tag 96) envelope read from src.
It decodes the envelope header from src (requiring the FEE typ), finds the recipient whose kid matches unwrap's key id, recovers the CEK through unwrap, and returns a streaming reader over the decrypted plaintext. Only the header is read up front; the detached ciphertext is streamed from src on demand.
Decryption is streaming: a non-EOF error from the returned reader (see fee/aesstream) means the plaintext is incomplete and must be discarded.
If the envelope carries no recipients (a COSE_Encrypt0), Decrypt returns ErrNoRecipientsInEnvelope — use DecryptWithCEK. If no recipient kid matches unwrap, it returns ErrNoMatchingRecipient without attempting an unwrap. If the matched recipient's wrapped CEK cannot be recovered (e.g. the wrong key), the unwrap error is returned and no plaintext reader is produced.
func DecryptWithCEK ¶
DecryptWithCEK is Decrypt with a caller-provided content-encryption key instead of one recovered from an in-envelope recipient — for when the CEK was obtained out of band (e.g. unwrapped by a custody service). It accepts either a COSE_Encrypt (tag 96) or a recipient-less COSE_Encrypt0 (tag 16); any recipients are ignored. cek must be 32 bytes (AES-256).
The caller retains ownership of cek: it is copied into the body cipher but neither retained nor wiped by this call.
func Encrypt ¶
func Encrypt(plaintext io.Reader, recipients []Recipient, opts ...EncryptOption) (io.ReadCloser, error)
Encrypt seals plaintext into a FEE envelope (a COSE_Encrypt, tag 96) addressed to recipients and returns a reader over the wire blob: the encoded envelope immediately followed by the detached STREAM ciphertext (envelope||ciphertext).
It generates a fresh content-encryption key (CEK) and base nonce, wraps the CEK to each recipient, and streams the plaintext through the chunked AES-256-GCM-STREAM body cipher, so both plaintext and ciphertext flow with O(chunk size) memory. The same CEK is wrapped to every recipient, so any one of them can recover the object. recipients must be non-empty (the generated CEK would otherwise be unrecoverable); a nil or invalid recipient is reported before any plaintext is read. To seal under a CEK you already hold, or a recipient-less envelope, use EncryptWithCEK.
Encryption runs in a background goroutine that feeds the returned reader, so a caller MUST either read it to EOF or Close it: Close aborts the goroutine. An encryption failure surfaces as a non-EOF error from the reader's Read.
func EncryptWithCEK ¶
func EncryptWithCEK(plaintext io.Reader, cek []byte, recipients []Recipient, opts ...EncryptOption) (io.ReadCloser, error)
EncryptWithCEK is Encrypt with a caller-provided content-encryption key instead of a freshly generated one — for when the CEK is managed out of band (derived deterministically, or issued by a custody service). cek must be 32 bytes (AES-256).
With one or more recipients it produces a COSE_Encrypt (tag 96) that also carries the wrapped CEK; with no recipients it produces a recipient-less COSE_Encrypt0 (tag 16). Pair it with DecryptWithCEK to recover without an in-envelope unwrap.
The caller MUST use a distinct cek per envelope (or keep reuse far below the birthday bound below). Unlike Encrypt, which draws a fresh CEK each call, this seals under a caller-supplied key — so the only cross-envelope nonce separation is the random base nonce, which is aesstream.BaseNonceSize (7) bytes. Under a reused CEK, two envelopes drawing the same base nonce reuse an AES-GCM (key, nonce) pair, which is catastrophic (keystream reuse + tag forgery). A 7-byte random nonce collides at a ~2^28-envelope birthday bound, so sealing on the order of 2^30 objects under one CEK makes a collision essentially certain. The wire format is fixed by the FEE spec, so this is a caller obligation, not something this package can enforce.
The caller retains ownership of cek: it is copied into the body cipher (and wrapped to any recipients) but neither retained nor wiped by this call.
Types ¶
type EncryptOption ¶
type EncryptOption func(*encryptConfig)
EncryptOption configures Encrypt and EncryptWithCEK.
func WithChunkSize ¶
func WithChunkSize(n int) EncryptOption
WithChunkSize sets the STREAM plaintext chunk size, in bytes. A value of 0 (or an unset option) selects aesstream.DefaultChunkSize (256 KiB); any other value must be in [aesstream.MinChunkSize, aesstream.MaxChunkSize]. The chosen size is recorded in the envelope, so Decrypt recovers it.
func WithContentLength ¶
func WithContentLength(n int64) EncryptOption
WithContentLength declares the total plaintext length in bytes. When set to a non-negative value, the envelope records the chunk count (advisory metadata that lets a range/seek consumer plan fetches from the header alone), and the returned reader fails with ErrContentLengthMismatch if the plaintext turns out to be a different length.
A negative n is treated as "unknown", identical to not calling this option: a caller propagating an unknown HTTP Content-Length as -1 gets the unset behavior (no recorded chunk count, no mismatch check) rather than an error. When unknown, the chunk count is omitted — the object still decrypts, and range decryption derives the geometry from the ciphertext length instead.
type Recipient ¶
type Recipient interface {
// contains filtered or unexported methods
}
Recipient wraps a content-encryption key (CEK) into an in-envelope COSE_Recipient entry: the wrapped CEK travels in the envelope, and recovery unwraps it with the matching key (see RecipientUnwrapper). Encrypt calls it once per recipient. For the alternative where the CEK is supplied out of band rather than carried in the envelope, see EncryptWithCEK and DecryptWithCEK.
Recipient is a sealed interface: the only implementations are the ones constructed by NewECDHESRecipient and NewA256KWRecipient, so the set of key-wrap algorithms that can appear in a FEE envelope stays controlled.
func NewA256KWRecipient ¶
NewA256KWRecipient returns a Recipient that wraps the CEK under kek with A256KW. kid names the KEK and is recorded in the recipient entry so an NewA256KWUnwrapper holding the same KEK can be matched to it; kid must be non-empty. kek must be 32 bytes: A256KW is AES-256 Key Wrap, and the recipient declares that algorithm, so a shorter key would misname the wrap.
Where the KEK is held by an external custody service that unwraps the CEK itself, use DecryptWithCEK on recovery instead of this recipient — the CEK then never needs to travel in the envelope.
func NewECDHESRecipient ¶
NewECDHESRecipient returns a Recipient that wraps the CEK to pub with ECDH-ES+A256KW: a fresh ephemeral X25519 key agreement derives a key-encryption key, which A256KW-wraps the CEK. pub must be an X25519 key (the only curve the scheme supports). kid names the recipient key — e.g. a DID verification method ID — and must be non-empty; the library treats it as opaque and records it in the recipient entry so an NewECDHESUnwrapper with the same kid can be matched to it.
type RecipientUnwrapper ¶
type RecipientUnwrapper interface {
// contains filtered or unexported methods
}
RecipientUnwrapper recovers a content-encryption key (CEK) from the in-envelope recipient that holds it. Decrypt uses keyID to find the matching recipient entry, then calls unwrap on it. (When the CEK is supplied out of band rather than carried in the envelope, use DecryptWithCEK and no unwrapper.)
RecipientUnwrapper is a sealed interface: the only implementations are the ones constructed by NewECDHESUnwrapper and NewA256KWUnwrapper, mirroring the wrap side.
func NewA256KWUnwrapper ¶
func NewA256KWUnwrapper(kid, kek []byte) RecipientUnwrapper
NewA256KWUnwrapper returns a RecipientUnwrapper that recovers a CEK wrapped under kek with A256KW. kid is the recipient key id to match — the same value passed to NewA256KWRecipient. kek must be the 32-byte KEK the CEK was wrapped under; a different KEK fails the unwrap with aeskw.ErrIntegrity.
func NewECDHESUnwrapper ¶
func NewECDHESUnwrapper(kid []byte, priv *ecdh.PrivateKey) RecipientUnwrapper
NewECDHESUnwrapper returns a RecipientUnwrapper that recovers a CEK wrapped to the X25519 public key matching priv. kid is the recipient key id to match — the same value passed to NewECDHESRecipient. priv must be an X25519 key.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package aeskw implements the AES Key Wrap algorithm (AES-KW) from RFC 3394.
|
Package aeskw implements the AES Key Wrap algorithm (AES-KW) from RFC 3394. |
|
Package aesstream implements the chunked AES-256-GCM STREAM body cipher used by the Filecoin Encryption Envelope (FEE).
|
Package aesstream implements the chunked AES-256-GCM STREAM body cipher used by the Filecoin Encryption Envelope (FEE). |
|
Package cose implements just enough of COSE (CBOR Object Signing and Encryption, RFC 9052) to encode and decode a COSE_Encrypt (CBOR tag 96) or COSE_Encrypt0 (CBOR tag 16) structure with a detached payload, and to build the Enc_structure that COSE feeds to an AEAD as Additional Authenticated Data (AAD).
|
Package cose implements just enough of COSE (CBOR Object Signing and Encryption, RFC 9052) to encode and decode a COSE_Encrypt (CBOR tag 96) or COSE_Encrypt0 (CBOR tag 16) structure with a detached payload, and to build the Enc_structure that COSE feeds to an AEAD as Additional Authenticated Data (AAD). |
|
Package ecdhkw implements ECDH-ES+A256KW key wrapping over X25519: it encrypts a content-encryption key (CEK) to a recipient's X25519 public key so that only the holder of the matching private key can recover it.
|
Package ecdhkw implements ECDH-ES+A256KW key wrapping over X25519: it encrypts a content-encryption key (CEK) to a recipient's X25519 public key so that only the holder of the matching private key can recover it. |
|
Package vectors holds the FEE (Filecoin Encryption Envelope) cross-implementation test vectors: fixed fixture blobs that must decrypt identically under this Go implementation (fee/cose, fee/aesstream, fee/ecdhkw, fee/aeskw) and under the TypeScript reference implementation, foc-encryption (github.com/Kubuxu/foc-encryption-demo, packages/ foc-encryption), pinned in pull-foc-encryption.sh.
|
Package vectors holds the FEE (Filecoin Encryption Envelope) cross-implementation test vectors: fixed fixture blobs that must decrypt identically under this Go implementation (fee/cose, fee/aesstream, fee/ecdhkw, fee/aeskw) and under the TypeScript reference implementation, foc-encryption (github.com/Kubuxu/foc-encryption-demo, packages/ foc-encryption), pinned in pull-foc-encryption.sh. |