780 lines
22 KiB
Go
780 lines
22 KiB
Go
// SPDX-License-Identifier: 0BSD
|
|
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
|
|
package destination
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/announce"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/common"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/identity"
|
|
"git.quad4.io/Networks/Reticulum-Go/pkg/packet"
|
|
"golang.org/x/crypto/curve25519"
|
|
)
|
|
|
|
const (
|
|
// Destination direction types
|
|
// The IN bit specifies that the destination can receive traffic.
|
|
// The OUT bit specifies that the destination can send traffic.
|
|
// A destination can be both IN and OUT.
|
|
IN = 0x01
|
|
OUT = 0x02
|
|
|
|
// Destination types
|
|
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
|
|
)
|
|
|
|
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
|
|
AutoCompress bool
|
|
}
|
|
|
|
type Transport interface {
|
|
GetConfig() *common.ReticulumConfig
|
|
GetInterfaces() map[string]common.NetworkInterface
|
|
RegisterDestination(hash []byte, dest interface{})
|
|
}
|
|
|
|
type IncomingLinkHandler func(pkt *packet.Packet, dest *Destination, transport interface{}, networkIface common.NetworkInterface) (interface{}, error)
|
|
|
|
var incomingLinkHandler IncomingLinkHandler
|
|
|
|
func RegisterIncomingLinkHandler(handler IncomingLinkHandler) {
|
|
incomingLinkHandler = handler
|
|
}
|
|
|
|
type Destination struct {
|
|
identity *identity.Identity
|
|
direction byte
|
|
destType byte
|
|
appName string
|
|
aspects []string
|
|
hashValue []byte
|
|
transport Transport
|
|
|
|
acceptsLinks bool
|
|
proofStrategy byte
|
|
|
|
packetCallback PacketCallback
|
|
proofCallback ProofRequestedCallback
|
|
linkCallback LinkEstablishedCallback
|
|
|
|
ratchetsEnabled bool
|
|
ratchetPath string
|
|
ratchetCount int
|
|
ratchetInterval int
|
|
enforceRatchets bool
|
|
latestRatchetTime time.Time
|
|
latestRatchetID []byte
|
|
ratchets [][]byte
|
|
ratchetFileLock sync.Mutex
|
|
|
|
defaultAppData []byte
|
|
mutex sync.RWMutex
|
|
|
|
requestHandlers map[string]*RequestHandler
|
|
}
|
|
|
|
func New(id *identity.Identity, direction byte, destType byte, appName string, transport Transport, aspects ...string) (*Destination, error) {
|
|
debug.Log(debug.DEBUG_INFO, "Creating new destination", "app", appName, "type", destType, "direction", direction)
|
|
|
|
if id == nil {
|
|
debug.Log(debug.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,
|
|
transport: transport,
|
|
acceptsLinks: false,
|
|
proofStrategy: PROVE_NONE,
|
|
ratchetCount: RATCHET_COUNT,
|
|
ratchetInterval: RATCHET_INTERVAL,
|
|
requestHandlers: make(map[string]*RequestHandler),
|
|
}
|
|
|
|
// Generate destination hash
|
|
d.hashValue = d.calculateHash()
|
|
debug.Log(debug.DEBUG_VERBOSE, "Created destination with hash", "hash", fmt.Sprintf("%x", d.hashValue))
|
|
|
|
// Auto-register with transport if direction is IN
|
|
if (direction & IN) != 0 {
|
|
transport.RegisterDestination(d.hashValue, d)
|
|
debug.Log(debug.DEBUG_INFO, "Destination auto-registered with transport", "hash", fmt.Sprintf("%x", d.hashValue))
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// FromHash creates a destination from a known hash (e.g., from an announce).
|
|
// This is used by clients to create destination objects for servers they've discovered.
|
|
func FromHash(hash []byte, id *identity.Identity, destType byte, transport Transport) (*Destination, error) {
|
|
debug.Log(debug.DEBUG_INFO, "Creating destination from hash", "hash", fmt.Sprintf("%x", hash))
|
|
|
|
if id == nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Cannot create destination: identity is nil")
|
|
return nil, errors.New("identity cannot be nil")
|
|
}
|
|
|
|
d := &Destination{
|
|
identity: id,
|
|
direction: OUT,
|
|
destType: destType,
|
|
hashValue: hash,
|
|
transport: transport,
|
|
acceptsLinks: false,
|
|
proofStrategy: PROVE_NONE,
|
|
ratchetCount: RATCHET_COUNT,
|
|
ratchetInterval: RATCHET_INTERVAL,
|
|
requestHandlers: make(map[string]*RequestHandler),
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_VERBOSE, "Created destination from hash", "hash", fmt.Sprintf("%x", hash))
|
|
return d, nil
|
|
}
|
|
|
|
func (d *Destination) calculateHash() []byte {
|
|
debug.Log(debug.DEBUG_TRACE, "Calculating hash for destination", "name", d.ExpandName())
|
|
|
|
// destination_hash = SHA256(name_hash_10bytes + identity_hash_16bytes)[:16]
|
|
// Identity hash is the truncated hash of the public key (16 bytes)
|
|
identityHash := identity.TruncatedHash(d.identity.GetPublicKey())
|
|
|
|
// Name hash is the FULL 32-byte SHA256, then we take first 10 bytes for concatenation
|
|
nameHashFull := sha256.Sum256([]byte(d.ExpandName()))
|
|
nameHash10 := nameHashFull[:10] // Only use 10 bytes
|
|
|
|
debug.Log(debug.DEBUG_ALL, "Identity hash", "hash", fmt.Sprintf("%x", identityHash))
|
|
debug.Log(debug.DEBUG_ALL, "Name hash (10 bytes)", "hash", fmt.Sprintf("%x", nameHash10))
|
|
|
|
// Concatenate name_hash (10 bytes) + identity_hash (16 bytes) = 26 bytes
|
|
combined := append(nameHash10, identityHash...)
|
|
|
|
// Then hash again and truncate to 16 bytes
|
|
finalHashFull := sha256.Sum256(combined)
|
|
finalHash := finalHashFull[:16]
|
|
|
|
debug.Log(debug.DEBUG_VERBOSE, "Calculated destination hash", "hash", fmt.Sprintf("%x", finalHash))
|
|
|
|
return finalHash
|
|
}
|
|
|
|
func (d *Destination) ExpandName() string {
|
|
name := d.appName
|
|
for _, aspect := range d.aspects {
|
|
name += "." + aspect
|
|
}
|
|
return name
|
|
}
|
|
|
|
func (d *Destination) Announce(pathResponse bool, tag []byte, attachedInterface common.NetworkInterface) error {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
debug.Log(debug.DEBUG_VERBOSE, "Announcing destination", "name", d.ExpandName(), "path_response", pathResponse)
|
|
|
|
appData := d.defaultAppData
|
|
|
|
// Create announce packet using announce package
|
|
announceObj, err := announce.New(d.identity, d.hashValue, d.ExpandName(), appData, pathResponse, d.transport.GetConfig())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create announce: %w", err)
|
|
}
|
|
|
|
packet := announceObj.GetPacket()
|
|
if packet == nil {
|
|
return errors.New("failed to create announce packet")
|
|
}
|
|
|
|
if pathResponse && tag != nil {
|
|
debug.Log(debug.DEBUG_INFO, "Sending path response announce", "tag", fmt.Sprintf("%x", tag))
|
|
}
|
|
|
|
if d.transport == nil {
|
|
return errors.New("transport not initialized")
|
|
}
|
|
|
|
var lastErr error
|
|
if attachedInterface != nil {
|
|
if attachedInterface.IsEnabled() && attachedInterface.IsOnline() {
|
|
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to attached interface", "name", attachedInterface.GetName())
|
|
if err := attachedInterface.Send(packet, ""); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on attached interface", "error", err)
|
|
lastErr = err
|
|
}
|
|
}
|
|
} else {
|
|
interfaces := d.transport.GetInterfaces()
|
|
for name, iface := range interfaces {
|
|
if iface.IsEnabled() && iface.IsOnline() {
|
|
debug.Log(debug.DEBUG_VERBOSE, "Sending announce to interface", "name", name)
|
|
if err := iface.Send(packet, ""); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to send announce on interface", "name", name, "error", err)
|
|
lastErr = err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return lastErr
|
|
}
|
|
|
|
func (d *Destination) AcceptsLinks(accepts bool) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
d.acceptsLinks = accepts
|
|
|
|
// Register with transport if accepting links
|
|
if accepts && d.transport != nil {
|
|
d.transport.RegisterDestination(d.hashValue, d)
|
|
debug.Log(debug.DEBUG_VERBOSE, "Destination registered with transport for link requests", "hash", fmt.Sprintf("%x", d.hashValue))
|
|
}
|
|
}
|
|
|
|
func (d *Destination) SetLinkEstablishedCallback(callback common.LinkEstablishedCallback) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
d.linkCallback = callback
|
|
}
|
|
|
|
func (d *Destination) GetLinkCallback() common.LinkEstablishedCallback {
|
|
d.mutex.RLock()
|
|
defer d.mutex.RUnlock()
|
|
return d.linkCallback
|
|
}
|
|
|
|
func (d *Destination) HandleIncomingLinkRequest(pkt interface{}, transport interface{}, networkIface common.NetworkInterface) error {
|
|
debug.Log(debug.DEBUG_INFO, "Handling incoming link request for destination", "hash", fmt.Sprintf("%x", d.GetHash()))
|
|
|
|
pktObj, ok := pkt.(*packet.Packet)
|
|
if !ok {
|
|
return errors.New("invalid packet type")
|
|
}
|
|
|
|
if incomingLinkHandler == nil {
|
|
return errors.New("no incoming link handler registered")
|
|
}
|
|
|
|
linkIface, err := incomingLinkHandler(pktObj, d, transport, networkIface)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to handle link request: %w", err)
|
|
}
|
|
|
|
if d.linkCallback != nil && linkIface != nil {
|
|
debug.Log(debug.DEBUG_INFO, "Calling link established callback")
|
|
d.linkCallback(linkIface)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Destination) SetPacketCallback(callback common.PacketCallback) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
d.packetCallback = callback
|
|
}
|
|
|
|
func (d *Destination) Receive(pkt *packet.Packet, iface common.NetworkInterface) {
|
|
d.mutex.RLock()
|
|
callback := d.packetCallback
|
|
d.mutex.RUnlock()
|
|
|
|
if callback == nil {
|
|
debug.Log(debug.DEBUG_VERBOSE, "No packet callback set for destination")
|
|
return
|
|
}
|
|
|
|
if pkt.PacketType == packet.PacketTypeLinkReq {
|
|
debug.Log(debug.DEBUG_INFO, "Received link request for destination")
|
|
if err := d.HandleIncomingLinkRequest(pkt, d.transport, iface); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to handle incoming link request", "error", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
plaintext, err := d.Decrypt(pkt.Data)
|
|
if err != nil {
|
|
debug.Log(debug.DEBUG_INFO, "Failed to decrypt packet data", "error", err)
|
|
return
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_INFO, "Destination received packet", "bytes", len(plaintext))
|
|
|
|
callback(plaintext, iface)
|
|
}
|
|
|
|
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()
|
|
|
|
if path == "" {
|
|
debug.Log(debug.DEBUG_ERROR, "No ratchet file path specified")
|
|
return false
|
|
}
|
|
|
|
d.ratchetsEnabled = true
|
|
d.ratchetPath = path
|
|
d.latestRatchetTime = time.Time{} // Zero time to force rotation
|
|
|
|
// Load or initialize ratchets
|
|
if err := d.reloadRatchets(); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to load ratchets", "error", err)
|
|
// Initialize empty ratchet list
|
|
d.ratchets = make([][]byte, 0)
|
|
if err := d.persistRatchets(); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to create initial ratchet file", "error", err)
|
|
return false
|
|
}
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_INFO, "Ratchets enabled", "path", 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) GetRequestHandler(pathHash []byte) func([]byte, []byte, []byte, []byte, *identity.Identity, time.Time) interface{} {
|
|
d.mutex.RLock()
|
|
defer d.mutex.RUnlock()
|
|
|
|
for _, handler := range d.requestHandlers {
|
|
handlerPathHash := identity.TruncatedHash([]byte(handler.Path))
|
|
if string(handlerPathHash) == string(pathHash) {
|
|
return func(pathHash []byte, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt time.Time) interface{} {
|
|
allowed := false
|
|
if handler.AllowMode == ALLOW_ALL {
|
|
allowed = true
|
|
} else if handler.AllowMode == ALLOW_LIST && remoteIdentity != nil {
|
|
remoteHash := remoteIdentity.Hash()
|
|
for _, allowedHash := range handler.AllowedList {
|
|
if string(remoteHash) == string(allowedHash) {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !allowed {
|
|
return nil
|
|
}
|
|
|
|
result := handler.ResponseGenerator(handler.Path, data, requestID, linkID, remoteIdentity, requestedAt.Unix())
|
|
if result == nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Destination) HandleRequest(path string, data []byte, requestID []byte, linkID []byte, remoteIdentity *identity.Identity, requestedAt int64) []byte {
|
|
d.mutex.RLock()
|
|
handler, exists := d.requestHandlers[path]
|
|
d.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
debug.Log(debug.DEBUG_INFO, "No handler registered for path", "path", path)
|
|
return []byte(">Not Found\n\nThe requested resource was not found.")
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_VERBOSE, "Calling request handler", "path", path)
|
|
result := handler.ResponseGenerator(path, data, requestID, linkID, remoteIdentity, requestedAt)
|
|
if result == nil {
|
|
return []byte(">Not Found\n\nThe requested resource was not found.")
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
|
|
if d.destType == PLAIN {
|
|
debug.Log(debug.DEBUG_VERBOSE, "Using plaintext transmission for PLAIN destination")
|
|
return plaintext, nil
|
|
}
|
|
|
|
if d.identity == nil {
|
|
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no identity available")
|
|
return nil, errors.New("no identity available for encryption")
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_VERBOSE, "Encrypting bytes for destination", "bytes", len(plaintext), "destType", d.destType)
|
|
|
|
switch d.destType {
|
|
case SINGLE:
|
|
recipientKey := d.identity.GetEncryptionKey()
|
|
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for single recipient", "key", fmt.Sprintf("%x", recipientKey[:8]))
|
|
return d.identity.Encrypt(plaintext, recipientKey)
|
|
case GROUP:
|
|
key := d.identity.GetCurrentRatchetKey()
|
|
if key == nil {
|
|
debug.Log(debug.DEBUG_INFO, "Cannot encrypt: no ratchet key available")
|
|
return nil, errors.New("no ratchet key available")
|
|
}
|
|
debug.Log(debug.DEBUG_VERBOSE, "Encrypting for group with ratchet key", "key", fmt.Sprintf("%x", key[:8]))
|
|
return d.identity.EncryptWithHMAC(plaintext, key)
|
|
default:
|
|
debug.Log(debug.DEBUG_INFO, "Unsupported destination type for encryption", "destType", 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
|
|
}
|
|
|
|
func (d *Destination) persistRatchets() error {
|
|
d.ratchetFileLock.Lock()
|
|
defer d.ratchetFileLock.Unlock()
|
|
|
|
if !d.ratchetsEnabled || d.ratchetPath == "" {
|
|
return errors.New("ratchets not enabled or no path specified")
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_PACKETS, "Persisting ratchets", "count", len(d.ratchets), "path", d.ratchetPath)
|
|
|
|
// Pack ratchets using msgpack
|
|
packedRatchets, err := common.MsgpackMarshal(d.ratchets)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pack ratchets: %w", err)
|
|
}
|
|
|
|
// Sign the packed ratchets
|
|
signature, err := d.Sign(packedRatchets)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to sign ratchets: %w", err)
|
|
}
|
|
|
|
// Create structure
|
|
persistedData := map[string][]byte{
|
|
"signature": signature,
|
|
"ratchets": packedRatchets,
|
|
}
|
|
|
|
// Pack the entire structure
|
|
finalData, err := common.MsgpackMarshal(persistedData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pack ratchet data: %w", err)
|
|
}
|
|
|
|
// Write to temporary file first, then rename (atomic operation)
|
|
tempPath := d.ratchetPath + ".tmp"
|
|
file, err := os.Create(tempPath) // #nosec G304
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp ratchet file: %w", err)
|
|
}
|
|
|
|
if _, err := file.Write(finalData); err != nil {
|
|
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
|
file.Close()
|
|
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
|
os.Remove(tempPath)
|
|
return fmt.Errorf("failed to write ratchet data: %w", err)
|
|
}
|
|
// #nosec G104 - File is being closed after successful write, error is non-critical
|
|
file.Close()
|
|
|
|
// Remove old file if exists
|
|
if _, err := os.Stat(d.ratchetPath); err == nil {
|
|
// #nosec G104 - Removing old file, error is non-critical if it doesn't exist
|
|
os.Remove(d.ratchetPath)
|
|
}
|
|
|
|
// Atomic rename
|
|
if err := os.Rename(tempPath, d.ratchetPath); err != nil {
|
|
// #nosec G104 - Error already being handled, cleanup errors are non-critical
|
|
os.Remove(tempPath)
|
|
return fmt.Errorf("failed to rename ratchet file: %w", err)
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_PACKETS, "Ratchets persisted successfully")
|
|
return nil
|
|
}
|
|
|
|
func (d *Destination) reloadRatchets() error {
|
|
d.ratchetFileLock.Lock()
|
|
defer d.ratchetFileLock.Unlock()
|
|
|
|
if _, err := os.Stat(d.ratchetPath); os.IsNotExist(err) {
|
|
debug.Log(debug.DEBUG_INFO, "No existing ratchet data found, initializing new ratchet file")
|
|
d.ratchets = make([][]byte, 0)
|
|
return nil
|
|
}
|
|
|
|
file, err := os.Open(d.ratchetPath) // #nosec G304
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open ratchet file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Read all data
|
|
fileData, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read ratchet file: %w", err)
|
|
}
|
|
|
|
// Unpack outer structure
|
|
var persistedData map[string][]byte
|
|
if err := common.MsgpackUnmarshal(fileData, &persistedData); err != nil {
|
|
return fmt.Errorf("failed to unpack ratchet data: %w", err)
|
|
}
|
|
|
|
signature, hasSignature := persistedData["signature"]
|
|
packedRatchets, hasRatchets := persistedData["ratchets"]
|
|
|
|
if !hasSignature || !hasRatchets {
|
|
return fmt.Errorf("invalid ratchet file format")
|
|
}
|
|
|
|
// Verify signature
|
|
if !d.identity.Verify(packedRatchets, signature) {
|
|
return fmt.Errorf("invalid ratchet file signature")
|
|
}
|
|
|
|
// Unpack ratchet list
|
|
if err := common.MsgpackUnmarshal(packedRatchets, &d.ratchets); err != nil {
|
|
return fmt.Errorf("failed to unpack ratchet list: %w", err)
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_INFO, "Ratchets reloaded successfully", "count", len(d.ratchets))
|
|
return nil
|
|
}
|
|
|
|
func (d *Destination) RotateRatchets() error {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
if !d.ratchetsEnabled {
|
|
return errors.New("ratchets not enabled")
|
|
}
|
|
|
|
now := time.Now()
|
|
if !d.latestRatchetTime.IsZero() && now.Before(d.latestRatchetTime.Add(time.Duration(d.ratchetInterval)*time.Second)) {
|
|
debug.Log(debug.DEBUG_TRACE, "Ratchet rotation interval not reached")
|
|
return nil
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_INFO, "Rotating ratchets", "destination", d.ExpandName())
|
|
|
|
// Generate new ratchet key (32 bytes for X25519 private key)
|
|
newRatchet := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, newRatchet); err != nil {
|
|
return fmt.Errorf("failed to generate new ratchet: %w", err)
|
|
}
|
|
|
|
// Insert at beginning (most recent first)
|
|
d.ratchets = append([][]byte{newRatchet}, d.ratchets...)
|
|
d.latestRatchetTime = now
|
|
|
|
// Get ratchet public key for ID
|
|
ratchetPub, err := curve25519.X25519(newRatchet, curve25519.Basepoint)
|
|
if err == nil {
|
|
d.latestRatchetID = identity.TruncatedHash(ratchetPub)[:identity.NAME_HASH_LENGTH/8]
|
|
}
|
|
|
|
// Clean old ratchets
|
|
d.cleanRatchets()
|
|
|
|
// Persist to disk
|
|
if err := d.persistRatchets(); err != nil {
|
|
debug.Log(debug.DEBUG_ERROR, "Failed to persist ratchets after rotation", "error", err)
|
|
return err
|
|
}
|
|
|
|
debug.Log(debug.DEBUG_INFO, "Ratchet rotation completed", "total_ratchets", len(d.ratchets))
|
|
return nil
|
|
}
|
|
|
|
func (d *Destination) cleanRatchets() {
|
|
if len(d.ratchets) > d.ratchetCount {
|
|
debug.Log(debug.DEBUG_TRACE, "Cleaning old ratchets", "before", len(d.ratchets), "keeping", d.ratchetCount)
|
|
d.ratchets = d.ratchets[:d.ratchetCount]
|
|
}
|
|
}
|
|
|
|
func (d *Destination) GetRatchets() [][]byte {
|
|
d.mutex.RLock()
|
|
defer d.mutex.RUnlock()
|
|
|
|
if !d.ratchetsEnabled {
|
|
return nil
|
|
}
|
|
|
|
// Return copy to prevent external modification
|
|
ratchetsCopy := make([][]byte, len(d.ratchets))
|
|
copy(ratchetsCopy, d.ratchets)
|
|
return ratchetsCopy
|
|
}
|