You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/capsule/padding.go

73 lines
2.3 KiB

Implement capsule format 2 of spec v0.9 The reference moves to the DateKeys Protocol Specification v0.9, approved by its author on 29 September 2026. Encrypt writes capsule format 2 only; Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the verdict v0.8.2 gave it. Format 2 (spec §22, §29.1, §31, §39): - VERSION in the PRELUDE is the capsule format, capsule.Format; any other value is ERR_UNSUPPORTED_VERSION at step 2. - CONTROL_CBOR has the schema version of its format. Version 2 adds key 6, payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without extensions, whatever L. - The payload is the content padded with zeros to P = rule(L). Step 17 checks the length and the zeros, and Open writes only the first L bytes. - INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials, and a dummy in each slot left, in a uniformly random order. Writer rules (spec §62.1): EncryptOptions.Length is required and the source must deliver exactly that many bytes; recipients that are not canonical or of low order are rejected (agewrap.CheckX25519Recipient); self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE. The CLI measures its input, takes -padding and reports the format. Test data: seven format 2 fixtures, padding vectors checked against math/big, format 2 CBOR vectors, and the mutation corpus in both formats with the 22 cases of the third list of spec §64, built without randomness by sealing the fixtures again with their known keys and nonces. The format 1 fixtures are kept byte for byte and never regenerated; the differential corpus keeps its 1825 cases and adds a block per format 2 fixture. The spec copy loses its "to be implemented" markers, and the READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
package capsule
import (
"fmt"
"math/bits"
)
// Padding is the padding rule of the payload of a format 2 capsule, sealed in
// key 7 of CONTROL_CBOR (spec §29.1, §31). No code stands for no padding.
type Padding uint8
// Padding rules of format 2 (spec §29.1).
const (
// Bloque256 pads the content to the next multiple of 256 bytes, and to
// at least 256.
Bloque256 Padding = 1
// Reforzado pads to the larger of Bloque256 and Padmé. It is the rule
// Encrypt uses by default, as spec §29.1 asks of the official SDK.
Reforzado Padding = 2
)
// MaxPayloadLength is L_MAX = 2^53 - 2^46, the largest content length a
// format 2 capsule can seal: the largest L for which both rules give a P of
// at most 2^53 - 1 (spec §29.1).
const MaxPayloadLength = 1<<53 - 1<<46
func (p Padding) String() string {
switch p {
case Bloque256:
return "bloque256"
case Reforzado:
return "reforzado"
}
return fmt.Sprintf("padding(%d)", uint8(p))
}
func (p Padding) valid() bool { return p == Bloque256 || p == Reforzado }
// PaddedLength returns P = rule(L), the exact length of the plaintext of
// PAYLOAD_AGE in a format 2 capsule whose content is l bytes long (spec
// §29.1). The arithmetic is exact and on 64-bit integers only, never a
// floating-point logarithm or 32-bit operations.
func PaddedLength(l uint64, p Padding) (uint64, error) {
if !p.valid() {
return 0, fmt.Errorf("capsule: padding code %d is not defined", uint8(p))
}
if l > MaxPayloadLength {
return 0, fmt.Errorf("capsule: content of %d bytes exceeds L_MAX = %d", l, uint64(MaxPayloadLength))
}
if l <= 256 {
return 256, nil
}
block := (l + 255) &^ 255
if p == Bloque256 {
return block, nil
}
// Padmé: keep the S + 1 most significant bits of L and round up the
// others, with E = floor(log2 L) and S = floor(log2 E) + 1.
e := uint64(bits.Len64(l) - 1)
s := uint64(bits.Len64(e))
mask := uint64(1)<<(e-s) - 1
return max(block, (l+mask)&^mask), nil
}
// PayloadAgeLength returns the length of a PAYLOAD_AGE whose plaintext is n
// bytes long: the 184 bytes of an age header with one X25519 stanza and the
// nonce, the plaintext, and the 16-byte tag of each 64 KiB STREAM chunk, at
// least one (spec §62.1, informative note).
func PayloadAgeLength(n uint64) uint64 {
chunks := max(1, (n+65535)/65536)
return 184 + n + 16*chunks
}

Powered by TurnKey Linux.