Files
vault/oracle/go/internal/pqcrypt/primitives.go
T
Andy DixonandClaude 69f4b90777 Freeze the Go implementation as a test-only oracle, with vectors
oracle/go is the Go code at 4c1507c, moved unchanged, plus test-only
generators that record what it computes. tests/vectors holds the
deterministic crypto/store vectors and synthetic fixtures they produce.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-08 09:55:17 +01:00

112 lines
4.0 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package pqcrypt
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"io"
"github.com/andydixon/vault/internal/format"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
func wipe(b []byte) {
for i := range b {
b[i] = 0
}
}
// aeadEncrypt encrypts a message with a one-time key.
//
// INVARIANT (fixed nonce). The all-zero nonce below is safe only because
// every key reaching this function is a fresh, unique, single-use derived
// KEK. It is not a shortcut and must not be reused anywhere that condition
// does not hold: repeating a (key, nonce) pair under ChaCha20-Poly1305
// destroys both confidentiality and authenticity.
//
// The condition holds today because aeadEncrypt has exactly one caller, the
// default suite's wrap, whose KEK is
// HKDF-SHA-256(ss1 || ss2, info = label || ct1 || ct2). Both KEM
// encapsulations are performed freshly for each wrap, so ss1, ss2, ct1 and
// ct2 differ for every call even when the same file key is wrapped to the
// same recipient twice. Any new caller must establish the same property, or
// carry its own nonce.
//
// For the code that encrypts the actual payload, which uses a counter nonce
// rather than a fixed one, see the
// github.com/andydixon/vault/internal/stream package.
func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
nonce := make([]byte, chacha20poly1305.NonceSize)
return aead.Seal(nil, nonce, plaintext, nil), nil
}
var errIncorrectCiphertextSize = errors.New("encrypted value has unexpected length")
// aeadDecrypt decrypts a message of an expected fixed size.
//
// The message size is limited to mitigate multi-key attacks, where a ciphertext
// can be crafted that decrypts successfully under multiple keys. Short
// ciphertexts can only target two keys, which has limited impact.
func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
if len(ciphertext) != size+aead.Overhead() {
return nil, errIncorrectCiphertextSize
}
nonce := make([]byte, chacha20poly1305.NonceSize)
return aead.Open(nil, nonce, ciphertext, nil)
}
// headerMAC computes the header authentication tag.
//
// INVARIANT (header authentication). The MAC covers the complete header
// except the MAC line itself: the intro line, every recipient stanza in
// order, and the footer prefix. Nothing that a decryptor acts upon is left
// unauthenticated, so a stanza cannot be added, removed, reordered or edited
// without detection by anyone who can recover the file key.
//
// INVARIANT (domain separation). The MAC key and the payload key are both
// HKDF-SHA-256 expansions of the same file key under distinct info strings
// ("header" and "payload"), so neither can be substituted for the other.
func headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) {
h := hkdf.New(sha256.New, fileKey, nil, []byte("header"))
hmacKey := make([]byte, 32)
if _, err := io.ReadFull(h, hmacKey); err != nil {
wipe(hmacKey)
return nil, err
}
// The MAC key is derived per call and is of no use once the hash is
// keyed, so it does not need to outlive this function.
defer wipe(hmacKey)
hh := hmac.New(sha256.New, hmacKey)
if err := hdr.MarshalWithoutMAC(hh); err != nil {
return nil, err
}
return hh.Sum(nil), nil
}
// streamKey derives the payload key.
//
// INVARIANT (payload key uniqueness). The 16-byte file nonce is fresh
// randomness written into every file and used here as the HKDF salt, so two
// encryptions never share a payload key even if a file key were somehow
// repeated. Combined with the STREAM counter nonce, no (key, nonce) pair is
// ever reused.
func streamKey(fileKey, nonce []byte) []byte {
h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload"))
streamKey := make([]byte, chacha20poly1305.KeySize)
if _, err := io.ReadFull(h, streamKey); err != nil {
panic("fury: internal error: failed to read from HKDF: " + err.Error())
}
return streamKey
}