Files
Reticulum-Go/pkg/destination/destination.go
Sudo-Ivan 5acbef454f 0.3.5
2025-01-01 18:31:58 -06:00

406 lines
10 KiB
Go

package destination
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"log"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
const (
IN = 0x01
OUT = 0x02
SINGLE = 0x00
GROUP = 0x01
PLAIN = 0x02
PROVE_NONE = 0x00
PROVE_ALL = 0x01
PROVE_APP = 0x02
ALLOW_NONE = 0x00
ALLOW_ALL = 0x01
ALLOW_LIST = 0x02
RATCHET_COUNT = 512 // Default number of retained ratchet keys
RATCHET_INTERVAL = 1800 // Minimum interval between ratchet rotations in seconds
// Debug levels
DEBUG_CRITICAL = 1 // Critical errors
DEBUG_ERROR = 2 // Non-critical errors
DEBUG_INFO = 3 // Important information
DEBUG_VERBOSE = 4 // Detailed information
DEBUG_TRACE = 5 // Very detailed tracing
DEBUG_PACKETS = 6 // Packet-level details
DEBUG_ALL = 7 // Everything
)
type PacketCallback = common.PacketCallback
type ProofRequestedCallback = common.ProofRequestedCallback
type LinkEstablishedCallback = common.LinkEstablishedCallback
type RequestHandler struct {
Path string
ResponseGenerator func(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte
AllowMode byte
AllowedList [][]byte
}
type Destination struct {
identity *identity.Identity
direction byte
destType byte
appName string
aspects []string
hashValue []byte
acceptsLinks bool
proofStrategy byte
packetCallback PacketCallback
proofCallback ProofRequestedCallback
linkCallback LinkEstablishedCallback
ratchetsEnabled bool
ratchetPath string
ratchetCount int
ratchetInterval int
enforceRatchets bool
defaultAppData []byte
mutex sync.RWMutex
requestHandlers map[string]*RequestHandler
}
func debugLog(level int, format string, v ...interface{}) {
log.Printf("[DEBUG-%d] %s", level, fmt.Sprintf(format, v...))
}
func New(id *identity.Identity, direction byte, destType byte, appName string, aspects ...string) (*Destination, error) {
debugLog(DEBUG_INFO, "Creating new destination: app=%s type=%d direction=%d", appName, destType, direction)
if id == nil {
debugLog(DEBUG_ERROR, "Cannot create destination: identity is nil")
return nil, errors.New("identity cannot be nil")
}
d := &Destination{
identity: id,
direction: direction,
destType: destType,
appName: appName,
aspects: aspects,
acceptsLinks: false,
proofStrategy: PROVE_NONE,
ratchetCount: RATCHET_COUNT,
ratchetInterval: RATCHET_INTERVAL,
requestHandlers: make(map[string]*RequestHandler),
}
// Generate destination hash
d.hashValue = d.calculateHash()
debugLog(DEBUG_VERBOSE, "Created destination with hash: %x", d.hashValue)
return d, nil
}
func (d *Destination) calculateHash() []byte {
debugLog(DEBUG_TRACE, "Calculating hash for destination %s", d.ExpandName())
nameHash := sha256.Sum256([]byte(d.ExpandName()))
identityHash := sha256.Sum256(d.identity.GetPublicKey())
debugLog(DEBUG_ALL, "Name hash: %x", nameHash)
debugLog(DEBUG_ALL, "Identity hash: %x", identityHash)
combined := append(nameHash[:], identityHash[:]...)
finalHash := sha256.Sum256(combined)
truncated := finalHash[:16]
debugLog(DEBUG_VERBOSE, "Calculated destination hash: %x", truncated)
return truncated
}
func (d *Destination) ExpandName() string {
name := d.appName
for _, aspect := range d.aspects {
name += "." + aspect
}
return name
}
func (d *Destination) Announce(appData []byte) error {
d.mutex.Lock()
defer d.mutex.Unlock()
log.Printf("[DEBUG-4] Creating announce packet for destination %s", d.ExpandName())
// If no specific appData provided, use default
if appData == nil {
log.Printf("[DEBUG-4] Using default app data for announce")
appData = d.defaultAppData
}
// Create announce packet
packet := make([]byte, 0, 256) // Pre-allocate reasonable size
// Add packet type and header
packet = append(packet, 0x01) // PACKET_TYPE_ANNOUNCE
packet = append(packet, 0x00) // Initial hop count
// Add destination hash (16 bytes)
packet = append(packet, d.hashValue...)
log.Printf("[DEBUG-4] Added destination hash %x to announce", d.hashValue[:8])
// Add identity public key (32 bytes)
pubKey := d.identity.GetPublicKey()
packet = append(packet, pubKey...)
log.Printf("[DEBUG-4] Added public key %x to announce", pubKey[:8])
// Add app data with length prefix
appDataLen := make([]byte, 2)
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
packet = append(packet, appDataLen...)
packet = append(packet, appData...)
log.Printf("[DEBUG-4] Added %d bytes of app data to announce", len(appData))
// Add ratchet data if enabled
if d.ratchetsEnabled {
log.Printf("[DEBUG-4] Adding ratchet data to announce")
ratchetKey := d.identity.GetCurrentRatchetKey()
if ratchetKey == nil {
log.Printf("[DEBUG-3] Failed to get current ratchet key")
return errors.New("failed to get current ratchet key")
}
packet = append(packet, ratchetKey...)
log.Printf("[DEBUG-4] Added ratchet key %x to announce", ratchetKey[:8])
}
// Sign the announce packet (64 bytes)
signData := append(d.hashValue, appData...)
if d.ratchetsEnabled {
signData = append(signData, d.identity.GetCurrentRatchetKey()...)
}
signature := d.identity.Sign(signData)
packet = append(packet, signature...)
log.Printf("[DEBUG-4] Added signature to announce packet (total size: %d bytes)", len(packet))
// Send announce packet through transport
log.Printf("[DEBUG-4] Sending announce packet through transport layer")
return transport.SendAnnounce(packet)
}
func (d *Destination) AcceptsLinks(accepts bool) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.acceptsLinks = accepts
}
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.linkCallback = callback
}
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.packetCallback = callback
}
func (d *Destination) SetProofRequestedCallback(callback common.ProofRequestedCallback) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.proofCallback = callback
}
func (d *Destination) SetProofStrategy(strategy byte) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.proofStrategy = strategy
}
func (d *Destination) EnableRatchets(path string) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetsEnabled = true
d.ratchetPath = path
return true
}
func (d *Destination) EnforceRatchets() {
d.mutex.Lock()
defer d.mutex.Unlock()
d.enforceRatchets = true
}
func (d *Destination) SetRetainedRatchets(count int) bool {
if count < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetCount = count
return true
}
func (d *Destination) SetRatchetInterval(interval int) bool {
if interval < 1 {
return false
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.ratchetInterval = interval
return true
}
func (d *Destination) SetDefaultAppData(data []byte) {
d.mutex.Lock()
defer d.mutex.Unlock()
d.defaultAppData = data
}
func (d *Destination) ClearDefaultAppData() {
d.mutex.Lock()
defer d.mutex.Unlock()
d.defaultAppData = nil
}
func (d *Destination) RegisterRequestHandler(path string, responseGen func(string, []byte, []byte, []byte, *identity.Identity, int64) []byte, allow byte, allowedList [][]byte) error {
if path == "" {
return errors.New("path cannot be empty")
}
if allow != ALLOW_NONE && allow != ALLOW_ALL && allow != ALLOW_LIST {
return errors.New("invalid allow mode")
}
if allow == ALLOW_LIST && len(allowedList) == 0 {
return errors.New("allowed list required for ALLOW_LIST mode")
}
d.mutex.Lock()
defer d.mutex.Unlock()
d.requestHandlers[path] = &RequestHandler{
Path: path,
ResponseGenerator: responseGen,
AllowMode: allow,
AllowedList: allowedList,
}
return nil
}
func (d *Destination) DeregisterRequestHandler(path string) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
if _, exists := d.requestHandlers[path]; exists {
delete(d.requestHandlers, path)
return true
}
return false
}
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
if d.destType == PLAIN {
log.Printf("[DEBUG-4] Using plaintext transmission for PLAIN destination")
return plaintext, nil
}
if d.identity == nil {
log.Printf("[DEBUG-3] Cannot encrypt: no identity available")
return nil, errors.New("no identity available for encryption")
}
log.Printf("[DEBUG-4] Encrypting %d bytes for destination type %d", len(plaintext), d.destType)
switch d.destType {
case SINGLE:
recipientKey := d.identity.GetPublicKey()
log.Printf("[DEBUG-4] Encrypting for single recipient with key %x", recipientKey[:8])
return d.identity.Encrypt(plaintext, recipientKey)
case GROUP:
key := d.identity.GetCurrentRatchetKey()
if key == nil {
log.Printf("[DEBUG-3] Cannot encrypt: no ratchet key available")
return nil, errors.New("no ratchet key available")
}
log.Printf("[DEBUG-4] Encrypting for group with ratchet key %x", key[:8])
return d.identity.EncryptWithHMAC(plaintext, key)
default:
log.Printf("[DEBUG-3] Unsupported destination type %d for encryption", d.destType)
return nil, errors.New("unsupported destination type for encryption")
}
}
func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
if d.destType == PLAIN {
return ciphertext, nil
}
if d.identity == nil {
return nil, errors.New("no identity available for decryption")
}
// Create empty ratchet receiver to get latest ratchet ID if available
ratchetReceiver := &common.RatchetIDReceiver{}
// Call Decrypt with full parameter list:
// - ciphertext: the encrypted data
// - ratchets: nil since we're not providing specific ratchets
// - enforceRatchets: false to allow fallback to normal decryption
// - ratchetIDReceiver: to receive the latest ratchet ID used
return d.identity.Decrypt(ciphertext, nil, false, ratchetReceiver)
}
func (d *Destination) Sign(data []byte) ([]byte, error) {
if d.identity == nil {
return nil, errors.New("no identity available")
}
signature := d.identity.Sign(data)
return signature, nil
}
func (d *Destination) GetPublicKey() []byte {
if d.identity == nil {
return nil
}
return d.identity.GetPublicKey()
}
func (d *Destination) GetIdentity() *identity.Identity {
return d.identity
}
func (d *Destination) GetType() byte {
return d.destType
}
func (d *Destination) GetHash() []byte {
d.mutex.RLock()
defer d.mutex.RUnlock()
if d.hashValue == nil {
d.mutex.RUnlock()
d.mutex.Lock()
defer d.mutex.Unlock()
if d.hashValue == nil {
d.hashValue = d.calculateHash()
}
}
return d.hashValue
}