keyed

package
v0.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package keyed holds the part of BRC-369 keyed content that every application keyed under a random scalar shares: the content key, its symmetric key and its commitment (BRC-369 sections 2.1 and 2.2), the check a holder runs on a key it has just unwrapped, and BRC-2's symmetric form, in which a key is wrapped and a certificate field is encrypted.

A content key is a random scalar, never derived from an identity or from anything else. The commitment is published; the key is released only to those meant to hold it. A holder that unwraps a key checks it against the commitment before anything uses it (CheckOpened), so that a wrong key names whoever wrapped it and not whoever encrypted under it.

SealSegment and OpenSegment encrypt a value under a content key as one BRC-369 section 2.3 segment, and EpochWrapKey, WrapToEpoch and UnwrapFromEpoch wrap a content key for the members of one group epoch, under a key derived from the epoch's symmetric key, the content id and the application's own domain string.

What a key encrypts, who it is released to and how a release is framed are the application's. This package writes no record and names no protocol.

Index

Examples

Constants

View Source
const (
	DomainSymmetric  = "metanet keyed content symmetric v1"
	DomainCommitment = "metanet keyed content commitment v1"
)

BRC-369 section 2's domain strings.

View Source
const (
	IVLen = 32
	// TagLen is the length of the authentication tag.
	TagLen = 16
	// Overhead is what the form adds to a plaintext.
	Overhead = IVLen + TagLen
	// WrappedKeyLen is the length of a wrapped 32-byte key.
	WrappedKeyLen = 32 + Overhead
)

The sizes of BRC-2's symmetric form as go-sdk writes it: a 32-byte IV, the ciphertext, and a 16-byte tag.

View Source
const (
	// SaltLen is the length of the per-content salt.
	SaltLen = 4
	// SegmentIVLen is the length of a segment's IV: the salt, then the
	// segment index as eight bytes big-endian.
	SegmentIVLen = 12
	// MaxSegment is the most plaintext one segment holds here: 1 MiB.
	// BRC-369 leaves the bound open; this is the one sealing in a single
	// segment is held to, so that one value never needs a second.
	MaxSegment = 1 << 20
)

The sizes of one BRC-369 section 2.3 segment.

Variables

View Source
var (
	// ErrScalar is a key that is not a scalar in [1, n-1].
	ErrScalar = errors.New("keyed: key is not a scalar in [1, n-1]")
	// ErrCommitment is a key that is not the committed one.
	ErrCommitment = errors.New("keyed: key does not match the commitment")
	// ErrUnwrap is a wrap that does not open, or opens to something that
	// is not 32 bytes.
	ErrUnwrap = errors.New("keyed: wrap does not open")
)

A holder's refusals (BRC-369 section 5.3), never a host's: a host cannot see a key.

View Source
var (
	// ErrSegment is a plaintext or a ciphertext outside a segment's
	// bounds: a plaintext of 1 to MaxSegment bytes, a ciphertext of 17 to
	// MaxSegment + TagLen.
	ErrSegment = errors.New("keyed: not the length of one segment")
	// ErrTag is a segment whose tag does not verify under the key.
	ErrTag = errors.New("keyed: segment tag does not verify under the key")
	// ErrDomain is an empty epoch-wrap domain string.
	ErrDomain = errors.New("keyed: empty domain string")
)

Refusals of one segment. ErrTag is a holder's (BRC-369 section 5.3's third check): a key that opens the commitment and not the ciphertext names whoever encrypted under it.

Functions

func CheckOpened

func CheckOpened(plaintext []byte, commitment [32]byte) ([32]byte, error)

CheckOpened is the tail of every unwrap, in its order: the plaintext is exactly 32 bytes (ErrUnwrap), a scalar (ErrScalar), and the committed one (ErrCommitment). The key it returns with an error must not be used.

Example

A publisher draws a key, publishes its commitment, and wraps the key to each holder. A holder that opens a wrap checks the key against the commitment before anything uses it: a wrong key then names whoever wrapped it, not whoever encrypted under it.

package main

import (
	"errors"
	"fmt"

	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/keyed"
)

func main() {
	// A fixed scalar so the example prints the same thing every time; a
	// publisher calls keyed.SampleKey(nil).
	k := goldentest.Fill(0x07)
	if err := keyed.CheckScalar(k); err != nil {
		fmt.Println(err)
		return
	}
	commitment := keyed.Commitment(k)
	symmetric := keyed.SymmetricKey(k)
	fmt.Println("the commitment says nothing about the key that encrypts:", commitment != symmetric)

	// The wrap: BRC-2's symmetric form under a key the two ends share. A
	// wallet's Encrypt writes the same form with an IV it draws itself.
	shared := goldentest.Fill(0x21)
	wrap, err := keyed.SymmetricSeal(shared[:], goldentest.Fill(0x80), k[:])
	fmt.Println("wrap:", len(wrap), "bytes", err)

	opened, err := keyed.SymmetricOpen(shared[:], wrap)
	if err != nil {
		fmt.Println(err)
		return
	}
	got, err := keyed.CheckOpened(opened, commitment)
	fmt.Println("the committed key:", got == k, err)

	// The same wrap against another commitment, and another key's wrap.
	other := goldentest.Fill(0x08)
	_, err = keyed.CheckOpened(opened, keyed.Commitment(other))
	fmt.Println("another commitment:", errors.Is(err, keyed.ErrCommitment))
	wrong := goldentest.Fill(0x22)
	_, err = keyed.SymmetricOpen(wrong[:], wrap)
	fmt.Println("another shared key opens it:", err == nil)
}
Output:
the commitment says nothing about the key that encrypts: true
wrap: 80 bytes <nil>
the committed key: true <nil>
another commitment: true
another shared key opens it: false

func CheckScalar

func CheckScalar(k [32]byte) error

CheckScalar refuses, as ErrScalar, a 32-byte big-endian value outside [1, n-1].

func Commitment

func Commitment(k [32]byte) [32]byte

Commitment is BRC-369 section 2.2's digest form: SHA-256 of the domain string and the scalar.

func EpochWrapKey added in v0.7.0

func EpochWrapKey(domain string, epochSymmetric, contentID [32]byte) [32]byte

EpochWrapKey is the key a content key is wrapped under for the members of one group epoch: SHA-256 of domain, the epoch's BRC-369 symmetric key (SymmetricKey of the epoch key) and the content id of what k encrypts. The epoch's symmetric key is an input to the hash, never a cipher key itself, so one wrapping key wraps one content key, and an application's own domain string keeps its wraps apart from every other application's use of the same epoch key. domain is the application's registered string, such as "<application> epoch wrap v1".

func OpenSegment added in v0.7.0

func OpenSegment(k [32]byte, salt [SaltLen]byte, ciphertext []byte) ([]byte, error)

OpenSegment reverses SealSegment. A ciphertext that cannot be one segment (TagLen bytes or fewer, or more than MaxSegment + TagLen) is ErrSegment, a key that is not a scalar ErrScalar, and a tag that does not verify ErrTag; no plaintext is returned unless the tag verifies (BRC-369 section 2.3 rule 5). A caller checks the key against its commitment (CheckOpened) first, so that a failure here names the encrypter and not the wrapper.

func SampleKey

func SampleKey(r io.Reader) ([32]byte, error)

SampleKey draws a key: a scalar uniform in [1, n-1] from r (crypto/rand.Reader when r is nil), never derived from anything (BRC-369 section 2.1 rule 1).

func SampleSalt added in v0.7.0

func SampleSalt(r io.Reader) ([SaltLen]byte, error)

SampleSalt draws a salt from r (crypto/rand.Reader when r is nil). A salt is drawn fresh for every content key (BRC-369 section 2.3 rule 3).

func SealSegment added in v0.7.0

func SealSegment(k [32]byte, salt [SaltLen]byte, plaintext []byte) ([]byte, error)

SealSegment encrypts plaintext under the content key k as one BRC-369 section 2.3 segment: AES-256-GCM under SymmetricKey(k), the IV SegmentIV(salt, 0), no associated data, the 16-byte tag after the ciphertext. The segment size is the plaintext's length, so the result is that length plus TagLen. It is the bytes a segmented sealer writes for a plaintext no longer than its segment size.

k must be a scalar (ErrScalar) and the plaintext 1 to MaxSegment bytes (ErrSegment). A key and salt used twice break both plaintexts without any key: k is fresh per piece of content, and the salt is fresh per key.

func SegmentIV added in v0.7.0

func SegmentIV(salt [SaltLen]byte, i uint64) [SegmentIVLen]byte

SegmentIV is BRC-369 section 2.3 rule 2: the salt followed by the segment index as eight bytes big-endian. The one segment SealSegment writes is index 0, so its IV is the salt and eight zero bytes.

func SymmetricKey

func SymmetricKey(k [32]byte) [32]byte

SymmetricKey is BRC-369 section 2.1 rule 3: SHA-256 of the domain string and the scalar. It is never derived from the commitment.

func SymmetricOpen

func SymmetricOpen(key, sealed []byte) ([]byte, error)

SymmetricOpen reverses SymmetricSeal through go-sdk's own symmetric key, so it opens exactly what a wallet's Decrypt opens under the same key.

func SymmetricSeal

func SymmetricSeal(key []byte, iv [IVLen]byte, plaintext []byte) ([]byte, error)

SymmetricSeal is BRC-2's symmetric form as go-sdk writes it: AES-256-GCM under key with a 32-byte IV and no associated data, written IV || ciphertext || tag. It is what a wallet's Encrypt returns, with the IV chosen by the caller in place of a random one: an IV must never be used twice under one key, so only a caller that draws it from a CSPRNG, or a vector that fixes it to be reproducible, calls this.

func UnwrapFromEpoch added in v0.7.0

func UnwrapFromEpoch(domain string, epochSymmetric, contentID [32]byte, wrap []byte, commitment [32]byte) ([32]byte, error)

UnwrapFromEpoch is a member's side: it opens wrap with the key derived from the symmetric key of the epoch the record names, and with no other, then runs CheckOpened against commitment. A wrap that is not WrappedKeyLen bytes or does not open is ErrUnwrap: a holder of another epoch's key derives another wrapping key, and the wrap does not open. An empty domain is ErrDomain.

func WrapToEpoch added in v0.7.0

func WrapToEpoch(domain string, epochSymmetric, contentID, k [32]byte, r io.Reader) ([]byte, error)

WrapToEpoch wraps the content key k under EpochWrapKey in BRC-2's symmetric form: a 32-byte IV from r (crypto/rand.Reader when r is nil), the 32-byte ciphertext and the tag, WrappedKeyLen bytes. An empty domain is ErrDomain and a key that is not a scalar ErrScalar.

Example

A keyed value for the members of one group epoch: a fresh content key seals the value as one segment, and the key is wrapped under a key derived from the epoch's symmetric key, the content id and the application's own domain string. A member opens the wrap with the key of the epoch the record names, checks the key against the commitment, then opens the segment.

package main

import (
	"errors"
	"fmt"

	"github.com/lightwebinc/bcommon/goldentest"
	"github.com/lightwebinc/bcommon/keyed"
)

func main() {
	const domain = "example epoch wrap v1" // the application's registered string
	k, salt := goldentest.Fill(0x07), [keyed.SaltLen]byte{1, 2, 3, 4}
	ct, err := keyed.SealSegment(k, salt, []byte("hello, room"))
	fmt.Println("ciphertext:", len(ct), "bytes", err)

	epochSym := keyed.SymmetricKey(goldentest.Fill(0x51)) // from the epoch key the group released
	contentID := goldentest.Fill(0xc0)                    // the application's hash of its record without the wrap
	wrap, err := keyed.WrapToEpochWithIV(domain, epochSym, contentID, k, goldentest.Fill(0x90))
	fmt.Println("wrap:", len(wrap), "bytes", err)

	got, err := keyed.UnwrapFromEpoch(domain, epochSym, contentID, wrap, keyed.Commitment(k))
	if err != nil {
		fmt.Println(err)
		return
	}
	pt, err := keyed.OpenSegment(got, salt, ct)
	fmt.Printf("%s %v\n", pt, err)

	// Another epoch's key, or another application's domain string, derives
	// another wrapping key, and the wrap does not open.
	_, err = keyed.UnwrapFromEpoch(domain, keyed.SymmetricKey(goldentest.Fill(0x52)), contentID, wrap, keyed.Commitment(k))
	fmt.Println("another epoch:", errors.Is(err, keyed.ErrUnwrap))
	_, err = keyed.UnwrapFromEpoch("other epoch wrap v1", epochSym, contentID, wrap, keyed.Commitment(k))
	fmt.Println("another domain:", errors.Is(err, keyed.ErrUnwrap))
}
Output:
ciphertext: 27 bytes <nil>
wrap: 80 bytes <nil>
hello, room <nil>
another epoch: true
another domain: true

func WrapToEpochWithIV added in v0.7.0

func WrapToEpochWithIV(domain string, epochSymmetric, contentID, k [32]byte, iv [IVLen]byte) ([]byte, error)

WrapToEpochWithIV is WrapToEpoch with the caller's IV. An IV must never be used twice under one wrapping key; outside a vector that fixes it to be reproducible, call WrapToEpoch.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL