Reticulum-Go/pkg/packet/packet.go
Ivan cc0b9edfc9
fix: keep PacketHash bound to each packet hashBuf
Value-copied packets no longer alias another packet's hash buffer when
rehashed. Refresh handshake wire vectors.
2026-08-29 21:29:12 -05:00

471 lines
13 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2026 Quad4.io
package packet
import (
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"time"
"quad4/reticulum-go/pkg/debug"
"quad4/reticulum-go/pkg/identity"
)
// Packet represents a Reticulum packet with header, destination, context and payload.
type Packet struct {
HeaderType byte
PacketType byte
TransportType byte
Context byte
ContextFlag byte
Hops byte
DestinationType byte
DestinationHash []byte
Destination any
TransportID []byte
Data []byte
Raw []byte
Packed bool
Sent bool
CreateReceipt bool
FromPacked bool
SentAt time.Time
PacketHash []byte
hashBuf [32]byte
RatchetID []byte
RSSI *float64
SNR *float64
Q *float64
Addresses []byte
Link any
hashValid bool
}
// hashableInto writes the wire bytes that participate in the packet hash into dst
// (reinitialized) and returns the resulting slice. dst must have capacity at least
// the preimage length (always <= MTU for valid packets).
func (p *Packet) hashableInto(dst []byte) []byte {
b := dst[:0]
b = append(b, p.Raw[0]&HashableFlagsMask)
if p.HeaderType == HeaderType2 {
start := TruncatedHashLength + 2
if len(p.Raw) > start {
b = append(b, p.Raw[start:]...)
}
} else if len(p.Raw) > 2 {
b = append(b, p.Raw[2:]...)
}
return b
}
func (p *Packet) hashablePreimageLen() int {
n := 1
if p.HeaderType == HeaderType2 {
start := TruncatedHashLength + 2
if len(p.Raw) > start {
n += len(p.Raw) - start
}
} else if len(p.Raw) > 2 {
n += len(p.Raw) - 2
}
return n
}
func nextRawWireCap(need int) int {
if need > MTU {
return need
}
const align = 64
rounded := (need + align - 1) / align * align
if rounded > MTU {
return MTU
}
return rounded
}
// PacketConfig holds the parameters used to create a new packet.
type PacketConfig struct {
DestType byte
Data []byte
PacketType byte
Context byte
TransportType byte
HeaderType byte
TransportID []byte
CreateReceipt bool
ContextFlag byte
}
// NewPacket builds a new Packet from the given config fields.
func NewPacket(destType byte, data []byte, packetType byte, context byte,
transportType byte, headerType byte, transportID []byte, createReceipt bool,
contextFlag byte) *Packet {
return &Packet{
HeaderType: headerType,
PacketType: packetType,
TransportType: transportType,
Context: context,
ContextFlag: contextFlag,
Hops: 0,
DestinationType: destType,
Data: data,
TransportID: transportID,
CreateReceipt: createReceipt,
Packed: false,
Sent: false,
FromPacked: false,
}
}
func (p *Packet) headerFlags() byte {
flags := byte(0)
flags |= (p.HeaderType << 6) & HeaderMaskHeaderType
flags |= (p.ContextFlag << 5) & HeaderMaskContextFlag
flags |= (p.TransportType << 4) & HeaderMaskTransportType
flags |= (p.DestinationType << 2) & HeaderMaskDestinationType
flags |= p.PacketType & HeaderMaskPacketType
return flags
}
func (p *Packet) Pack() error {
if p.Packed {
return nil
}
if debug.Enabled(debug.DebugPackets) {
debug.Log(debug.DebugPackets, "Packing packet", "type", p.PacketType, "header", p.HeaderType)
}
if n := len(p.DestinationHash); n != 0 && n != TruncatedHashLength {
return fmt.Errorf("destination hash must be %d bytes, got %d", TruncatedHashLength, n)
}
if p.HeaderType == HeaderType2 {
if p.TransportID == nil {
return errors.New("transport ID required for header type 2")
}
if len(p.TransportID) != TruncatedHashLength {
return fmt.Errorf("transport ID must be %d bytes, got %d", TruncatedHashLength, len(p.TransportID))
}
}
flags := p.headerFlags()
if debug.Enabled(debug.DebugTrace) {
debug.Log(debug.DebugTrace, "Created packet header", "flags", fmt.Sprintf("%08b", flags), "hops", p.Hops)
}
need := 2 + len(p.DestinationHash) + 1 + len(p.Data)
if p.HeaderType == HeaderType2 {
need += len(p.TransportID)
if debug.Enabled(debug.DebugAll) {
debug.Log(debug.DebugAll, "Added transport ID to header", "transport_id", fmt.Sprintf("%x", p.TransportID))
}
}
destHash := p.DestinationHash
transportID := p.TransportID
payload := p.Data
var raw []byte
if cap(p.Raw) >= need {
// Unpack leaves DestinationHash, TransportID, and Data as views into Raw.
// Reusing that buffer would clobber those views while packing (especially
// HT1 to HT2 upgrades used by multi-hop SendPacket rewrap).
destHash = append([]byte(nil), destHash...)
if len(transportID) > 0 {
transportID = append([]byte(nil), transportID...)
}
payload = append([]byte(nil), payload...)
p.DestinationHash = destHash
p.TransportID = transportID
p.Data = payload
raw = p.Raw[:0]
} else {
newCap := need
if cap(p.Raw) > 0 {
newCap = nextRawWireCap(need)
}
raw = make([]byte, 0, newCap)
}
raw = append(raw, flags, p.Hops)
if p.HeaderType == HeaderType2 {
raw = append(raw, transportID...)
}
raw = append(raw, destHash...)
raw = append(raw, p.Context)
raw = append(raw, payload...)
p.Raw = raw
if debug.Enabled(debug.DebugPackets) {
hdrLen := 2 + len(destHash) + 1
if p.HeaderType == HeaderType2 {
hdrLen += len(transportID)
}
debug.Log(debug.DebugPackets, "Final header length", "bytes", hdrLen)
}
if debug.Enabled(debug.DebugTrace) {
debug.Log(debug.DebugTrace, "Final packet size", "bytes", len(p.Raw))
}
if len(p.Raw) > MTU {
return errors.New("packet size exceeds MTU")
}
p.Packed = true
p.hashValid = false
p.updateHash()
if debug.Enabled(debug.DebugAll) {
debug.Log(debug.DebugAll, "Packet hash", "hash", fmt.Sprintf("%x", p.PacketHash))
}
return nil
}
// PrepareHT1Buffer writes the header type 1 prefix into Raw and returns the
// payload region for in-place encryption or a copy. destHash must be
// TruncatedHashLength bytes and must not alias Raw.
func (p *Packet) PrepareHT1Buffer(destHash []byte, payloadLen int) ([]byte, error) {
if len(destHash) != TruncatedHashLength {
return nil, fmt.Errorf("destination hash must be %d bytes, got %d", TruncatedHashLength, len(destHash))
}
if p.HeaderType != HeaderType1 {
return nil, errors.New("PrepareHT1Buffer requires header type 1")
}
need := HeaderType1Overhead + payloadLen
if need > MTU {
return nil, errors.New("packet size exceeds MTU")
}
raw := p.Raw
if cap(raw) < need {
raw = make([]byte, need, nextRawWireCap(need))
} else {
raw = raw[:need]
}
raw[0] = p.headerFlags()
raw[1] = p.Hops
copy(raw[2:2+TruncatedHashLength], destHash)
raw[2+TruncatedHashLength] = p.Context
p.Raw = raw
p.DestinationHash = destHash
p.Data = raw[HeaderType1Overhead:]
p.Packed = false
p.hashValid = false
return p.Data, nil
}
// CommitPacked hashes a Raw buffer previously filled by PrepareHT1Buffer.
func (p *Packet) CommitPacked() error {
if len(p.Raw) > MTU {
return errors.New("packet size exceeds MTU")
}
p.Packed = true
p.hashValid = false
p.updateHash()
return nil
}
func (p *Packet) Unpack() error {
if len(p.Raw) < MinPacketSize {
return errors.New("packet too short")
}
if len(p.Raw) > MaxInboundPacketSize {
return errors.New("packet exceeds maximum inbound size")
}
flags := p.Raw[0]
p.Hops = p.Raw[1]
if int(p.Hops) >= PathfinderM {
return fmt.Errorf("invalid hop count %d", p.Hops)
}
p.HeaderType = (flags & HeaderMaskHeaderType) >> 6
p.ContextFlag = (flags & HeaderMaskContextFlag) >> 5
p.TransportType = (flags & HeaderMaskTransportType) >> 4
p.DestinationType = (flags & HeaderMaskDestinationType) >> 2
p.PacketType = flags & HeaderMaskPacketType
dstLen := TruncatedHashLength
if p.HeaderType == HeaderType2 {
if len(p.Raw) < 2*dstLen+MinPacketSize {
return errors.New("packet too short for header type 2")
}
p.TransportID = p.Raw[2 : dstLen+2]
p.DestinationHash = p.Raw[dstLen+2 : 2*dstLen+2]
p.Context = p.Raw[2*dstLen+2]
p.Data = p.Raw[2*dstLen+3:]
} else {
if len(p.Raw) < dstLen+MinPacketSize {
return errors.New("packet too short for header type 1")
}
p.TransportID = nil
p.DestinationHash = p.Raw[2 : dstLen+2]
p.Context = p.Raw[dstLen+2]
p.Data = p.Raw[dstLen+3:]
}
p.Packed = false
p.hashValid = false
p.updateHash()
return nil
}
func (p *Packet) GetHash() []byte {
if !p.hashValid {
p.updateHash()
}
return p.PacketHash
}
func (p *Packet) updateHash() {
n := p.hashablePreimageLen()
var sum [sha256.Size]byte
if n <= MTU {
var scratch [MTU]byte
hb := p.hashableInto(scratch[:0])
sum = sha256.Sum256(hb)
} else {
scratch := make([]byte, n)
hb := p.hashableInto(scratch[:0])
sum = sha256.Sum256(hb)
}
// Always bind PacketHash to this packet's hashBuf so a value-copied
// Packet cannot keep writing into another packet's buffer.
p.PacketHash = p.hashBuf[:]
copy(p.PacketHash, sum[:])
p.hashValid = true
}
func (p *Packet) Hash() []byte {
return p.GetHash()
}
func (p *Packet) TruncatedHash() []byte {
hash := p.GetHash()
if len(hash) >= TruncatedHashLength {
return hash[:TruncatedHashLength]
}
return hash
}
// LinkIDFromLinkRequest returns the link ID for a link request packet,
// matching RNS.Link.link_id_from_lr_packet.
func LinkIDFromLinkRequest(p *Packet) []byte {
if p == nil || len(p.Raw) == 0 {
return nil
}
hashable := p.hashableInto(nil)
if len(p.Data) > LinkRequestECPubSize {
diff := len(p.Data) - LinkRequestECPubSize
if len(hashable) >= diff {
hashable = hashable[:len(hashable)-diff]
}
}
return identity.TruncatedHash(hashable)
}
func (p *Packet) Serialize() ([]byte, error) {
if !p.Packed {
if err := p.Pack(); err != nil {
return nil, fmt.Errorf("failed to pack packet: %w", err)
}
}
p.Addresses = p.DestinationHash
return p.Raw, nil
}
func NewAnnouncePacket(destHash []byte, identity *identity.Identity, appData []byte, transportID []byte) (*Packet, error) {
debug.Log(debug.DebugAll, "Creating new announce packet", "dest_hash", fmt.Sprintf("%x", destHash), "app_data", fmt.Sprintf("%x", appData))
// Get public key separated into encryption and signing keys
pubKey := identity.GetPublicKey()
encKey := pubKey[:32]
signKey := pubKey[32:]
debug.Log(debug.DebugPackets, "Using public keys", "enc_key", fmt.Sprintf("%x", encKey), "sign_key", fmt.Sprintf("%x", signKey))
// Parse app name from first msgpack element if possible
// For nodes, we'll use "reticulum.node" as the name hash
var appName string
if len(appData) > 2 && appData[0] == 0x93 {
// This is a node announce, use standard node name
appName = "reticulum.node"
} else if len(appData) > 3 && appData[0] == 0x92 && appData[1] == 0xc4 {
// Try to extract name from peer announce appData
nameLen := int(appData[2])
if 3+nameLen <= len(appData) {
appName = string(appData[3 : 3+nameLen])
} else {
// Default fallback
appName = "reticulum-go.node"
}
} else {
// Default fallback
appName = "reticulum-go.node"
}
// Create name hash (10 bytes)
nameHash := sha256.Sum256([]byte(appName))
nameHash10 := nameHash[:10]
debug.Log(debug.DebugPackets, "Using name hash", "name", appName, "hash", fmt.Sprintf("%x", nameHash10))
// Create random hash (10 bytes) - 5 bytes random + 5 bytes time
randomHash := make([]byte, 10)
_, err := rand.Read(randomHash[:5]) // #nosec G104
if err != nil {
debug.Log(debug.DebugPackets, "Failed to read random bytes for hash", "error", err)
return nil, err // Or handle the error appropriately
}
timeBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix())) // #nosec G115
copy(randomHash[5:], timeBytes[3:8])
debug.Log(debug.DebugPackets, "Generated random hash", "hash", fmt.Sprintf("%x", randomHash))
signedData := make([]byte, 0, len(destHash)+len(encKey)+len(signKey)+len(nameHash10)+len(randomHash)+len(appData))
signedData = append(signedData, destHash...)
signedData = append(signedData, encKey...)
signedData = append(signedData, signKey...)
signedData = append(signedData, nameHash10...)
signedData = append(signedData, randomHash...)
signedData = append(signedData, appData...)
debug.Log(debug.DebugTrace, "Created signed data", "bytes", len(signedData))
signature, err := identity.Sign(signedData)
if err != nil {
return nil, fmt.Errorf("sign announce: %w", err)
}
debug.Log(debug.DebugPackets, "Generated signature", "signature", fmt.Sprintf("%x", signature))
data := make([]byte, 0, 32+32+10+10+64+len(appData))
data = append(data, encKey...)
data = append(data, signKey...)
data = append(data, nameHash10...)
data = append(data, randomHash...)
data = append(data, signature...)
data = append(data, appData...)
debug.Log(debug.DebugTrace, "Combined packet data", "bytes", len(data))
p := &Packet{
HeaderType: HeaderType2,
PacketType: PacketTypeAnnounce,
TransportID: transportID,
DestinationHash: destHash,
Data: data,
}
debug.Log(debug.DebugVerbose, "Created announce packet", "type", p.PacketType, "header", p.HeaderType)
return p, nil
}