This commit is contained in:
Sudo-Ivan
2024-12-30 03:50:52 -06:00
parent 0f5f5cbb13
commit decbd8f29a
16 changed files with 1046 additions and 970 deletions

40
To-Do
View File

@@ -17,17 +17,25 @@ Core Components
[✓] Identity creation
[✓] Key pair generation
[✓] Identity storage/recall
[✓] Public key handling
[✓] Signature verification
[✓] Hash functions
[✓] Cryptographic Primitives
[✓] Ed25519
[✓] Curve25519
[✓] AES-GCM
[✓] SHA-256
[✓] HKDF
[✓] Secure random number generation
[✓] HMAC
[✓] Packet Handling
[✓] Packet creation
[✓] Packet validation
[✓] Basic proof system
[✓] Crypto Implementation
[✓] Basic encryption
[✓] Key exchange
[✓] Hash functions
[✓] Ratchet implementation
[✓] Packet encryption
[✓] Signature verification
[✓] Transport Layer
[✓] Path management
@@ -52,6 +60,8 @@ Core Components
[✓] Encryption/Decryption
[✓] Identity verification
[✓] Request/Response handling
[✓] Session key management
[✓] Link state tracking
[✓] Resource System
[✓] Resource creation
@@ -61,6 +71,16 @@ Core Components
[✓] Segmentation
[✓] Cleanup routines
Security Features
[✓] Cryptographic Implementation
[✓] Secure key generation
[✓] Key exchange protocols
[✓] Message encryption
[✓] Signature schemes
[✓] Hash functions
[ ] Perfect forward secrecy
[ ] Post-quantum resistance considerations
Basic Features
[✓] Network Interface
[✓] Basic UDP transport
@@ -74,6 +94,7 @@ Basic Features
[✓] Announce creation
[✓] Announce propagation
[✓] Path requests
[✓] Announce validation
[✓] Resource Management
[✓] Resource tracking
@@ -86,6 +107,7 @@ Basic Features
[✓] Interactive mode
[✓] Link establishment
[✓] Message sending/receiving
[✓] Identity management
Next Immediate Tasks:
1. [✓] Fix import cycles by creating common package
@@ -100,3 +122,9 @@ Next Immediate Tasks:
10. [ ] Add metrics collection for interfaces
11. [ ] Implement client-side path caching
12. [ ] Add support for additional transport types
13. [ ] Implement perfect forward secrecy
14. [ ] Add post-quantum cryptographic primitives
15. [ ] Implement secure key rotation
16. [ ] Add support for encrypted storage of identities
17. [ ] Implement secure memory handling
18. [ ] Add support for hardware security modules

View File

@@ -2,6 +2,7 @@ package main
import (
"bufio"
"encoding/binary"
"flag"
"fmt"
"log"
@@ -10,14 +11,15 @@ import (
"strings"
"syscall"
"time"
"encoding/binary"
"github.com/Sudo-Ivan/reticulum-go/internal/config"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
"github.com/Sudo-Ivan/reticulum-go/pkg/interfaces"
"github.com/Sudo-Ivan/reticulum-go/pkg/announce"
"github.com/Sudo-Ivan/reticulum-go/pkg/link"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
var (
@@ -61,6 +63,7 @@ func NewClient(cfg *common.ReticulumConfig) (*Client, error) {
}
func (c *Client) Start() error {
// Initialize interfaces
for _, ifaceConfig := range c.config.Interfaces {
var iface common.NetworkInterface
@@ -74,14 +77,8 @@ func (c *Client) Start() error {
ifaceConfig.I2PTunneled,
)
if err != nil {
log.Printf("Failed to create TCP interface %s: %v", ifaceConfig.Name, err)
continue
return fmt.Errorf("failed to create TCP interface %s: %v", ifaceConfig.Name, err)
}
callback := common.PacketCallback(func(data []byte, iface interface{}) {
c.handlePacket(data, iface)
})
client.SetPacketCallback(callback)
iface = client
case "UDPInterface":
@@ -92,47 +89,23 @@ func (c *Client) Start() error {
"", // No target address for client initially
)
if err != nil {
log.Printf("Failed to create UDP interface %s: %v", ifaceConfig.Name, err)
continue
return fmt.Errorf("failed to create UDP interface %s: %v", ifaceConfig.Name, err)
}
callback := common.PacketCallback(func(data []byte, iface interface{}) {
c.handlePacket(data, iface)
})
udp.SetPacketCallback(callback)
iface = udp
case "AutoInterface":
log.Printf("AutoInterface type not yet implemented")
continue
default:
log.Printf("Unknown interface type: %s", ifaceConfig.Type)
continue
return fmt.Errorf("unsupported interface type: %s", ifaceConfig.Type)
}
if iface != nil {
c.interfaces = append(c.interfaces, iface)
}
log.Printf("Created interface %s", iface.GetName())
}
// Start periodic announce after interfaces are set up
// Start periodic announces
go func() {
// Initial delay to allow interfaces to connect
time.Sleep(5 * time.Second)
// Send first announce
c.sendAnnounce()
// Set up periodic announces
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.sendAnnounce()
}
time.Sleep(30 * time.Second)
}
}()
@@ -140,7 +113,7 @@ func (c *Client) Start() error {
return nil
}
func (c *Client) handlePacket(data []byte, iface interface{}) {
func (c *Client) handlePacket(data []byte, p *packet.Packet) {
if len(data) < 1 {
return
}
@@ -150,7 +123,7 @@ func (c *Client) handlePacket(data []byte, iface interface{}) {
case 0x04: // Announce packet
c.handleAnnounce(data[1:])
default:
c.transport.HandlePacket(data, iface)
c.transport.HandlePacket(data, p)
}
}
@@ -181,35 +154,45 @@ func (c *Client) handleAnnounce(data []byte) {
}
func (c *Client) sendAnnounce() {
// Create announce packet following RNS protocol
announceData := make([]byte, 0, 128)
announceData = append(announceData, 0x04) // Announce packet type
announceData = append(announceData, c.identity.Hash()...) // Identity hash (32 bytes)
announceData := make([]byte, 0)
// Add timestamp (8 bytes, big-endian)
timestamp := time.Now().Unix()
// Packet type (1 byte)
announceData = append(announceData, 0x04)
// Destination hash (16 bytes)
destHash := identity.TruncatedHash(c.identity.GetPublicKey())
announceData = append(announceData, destHash...)
// Timestamp (8 bytes)
timeBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timeBytes, uint64(timestamp))
binary.BigEndian.PutUint64(timeBytes, uint64(time.Now().Unix()))
announceData = append(announceData, timeBytes...)
// Add hops (1 byte)
announceData = append(announceData, 0x00) // Initial hop count
// Hops (1 byte)
announceData = append(announceData, 0x00)
// Add flags (1 byte)
announceData = append(announceData, byte(announce.ANNOUNCE_IDENTITY)) // Using identity announce type
// Flags (1 byte)
announceData = append(announceData, 0x00)
// Add app data with length prefix
// Public key
announceData = append(announceData, c.identity.GetPublicKey()...)
// App data with length prefix
appData := []byte("RNS.Go.Client")
lenBytes := make([]byte, 2)
binary.BigEndian.PutUint16(lenBytes, uint16(len(appData)))
announceData = append(announceData, lenBytes...)
announceData = append(announceData, appData...)
// Sign the announce packet
signature := c.identity.Sign(announceData)
// Sign the announce data
signData := append(destHash, c.identity.GetPublicKey()...)
signData = append(signData, appData...)
signature := c.identity.Sign(signData)
announceData = append(announceData, signature...)
log.Printf("Sending announce packet, length: %d bytes", len(announceData))
log.Printf("Sending announce for identity: %s", c.identity.Hex())
log.Printf("Announce packet length: %d bytes", len(announceData))
log.Printf("Announce packet hex: %x", announceData)
for _, iface := range c.interfaces {
if err := iface.Send(announceData, ""); err != nil {
@@ -227,6 +210,54 @@ func (c *Client) Stop() {
c.transport.Close()
}
func (c *Client) Connect(destHash []byte) error {
// Recall server identity
serverIdentity, err := identity.Recall(destHash)
if err != nil {
return err
}
// Create destination
dest, err := destination.New(
serverIdentity,
destination.OUT,
destination.SINGLE,
"example_utilities",
"identifyexample",
)
if err != nil {
return err
}
// Create link with all required parameters
link := link.NewLink(
dest,
c.transport, // Add the transport instance
c.handleLinkEstablished,
c.handleLinkClosed,
)
// Set callbacks
link.SetPacketCallback(c.handlePacket)
return nil
}
func (c *Client) handleLinkEstablished(l *link.Link) {
log.Printf("Link established with server, identifying...")
// Identify to server
if err := l.Identify(c.identity); err != nil {
log.Printf("Failed to identify: %v", err)
l.Teardown()
return
}
}
func (c *Client) handleLinkClosed(l *link.Link) {
log.Printf("Link closed")
}
func main() {
flag.Parse()

View File

@@ -60,8 +60,9 @@ func (r *Reticulum) Start() error {
ifaceConfig.Name,
ifaceConfig.Address,
ifaceConfig.Port,
ifaceConfig.PreferIPv6,
ifaceConfig.KISSFraming,
ifaceConfig.I2PTunneled,
ifaceConfig.PreferIPv6,
)
if err != nil {
log.Printf("Failed to create TCP server interface %s: %v", ifaceConfig.Name, err)

View File

@@ -4,8 +4,8 @@ import (
"os"
"path/filepath"
"github.com/pelletier/go-toml"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/pelletier/go-toml"
)
const (
@@ -88,6 +88,14 @@ func CreateDefaultConfig(path string) error {
TargetPort: 4242,
}
// Add default UDP interface
cfg.Interfaces["local udp"] = &common.InterfaceConfig{
Type: "UDPInterface",
Enabled: true,
Address: "0.0.0.0",
Port: 37428, // Default RNS port
}
data, err := toml.Marshal(cfg)
if err != nil {
return err

View File

@@ -6,11 +6,6 @@ import (
"time"
)
type InterfaceMode byte
type InterfaceType byte
type PacketCallback = func([]byte, interface{})
// NetworkInterface combines both low-level and high-level interface requirements
type NetworkInterface interface {
// Low-level network operations

View File

@@ -18,7 +18,7 @@ type Path struct {
}
// Common callbacks
type ProofRequestedCallback func(interface{}) bool
type ProofRequestedCallback func([]byte, []byte)
type LinkEstablishedCallback func(interface{})
// Request handler
@@ -28,3 +28,9 @@ type RequestHandler struct {
AllowMode byte
AllowedList [][]byte
}
type InterfaceMode byte
type InterfaceType byte
// PacketCallback defines the function signature for packet handling
type PacketCallback func([]byte, interface{})

View File

@@ -314,9 +314,15 @@ func (d *Destination) Encrypt(plaintext []byte) ([]byte, error) {
switch d.destType {
case SINGLE:
return d.identity.Encrypt(plaintext, nil)
// For single destination, we need the recipient's public key
recipientKey := d.identity.GetPublicKey()
return d.identity.Encrypt(plaintext, recipientKey)
case GROUP:
return d.identity.EncryptSymmetric(plaintext)
key := d.identity.GetCurrentRatchetKey()
if key == nil {
return nil, errors.New("no ratchet key available")
}
return d.identity.EncryptSymmetric(plaintext, key)
default:
return nil, errors.New("unsupported destination type for encryption")
}
@@ -333,9 +339,13 @@ func (d *Destination) Decrypt(ciphertext []byte) ([]byte, error) {
switch d.destType {
case SINGLE:
return d.identity.Decrypt(ciphertext, nil)
return d.identity.Decrypt(ciphertext)
case GROUP:
return d.identity.DecryptSymmetric(ciphertext)
key := d.identity.GetCurrentRatchetKey()
if key == nil {
return nil, errors.New("no ratchet key available")
}
return d.identity.DecryptSymmetric(ciphertext, key)
default:
return nil, errors.New("unsupported destination type for decryption")
}
@@ -348,3 +358,14 @@ func (d *Destination) Sign(data []byte) ([]byte, error) {
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
}

View File

@@ -4,21 +4,17 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"sync"
"time"
"encoding/hex"
"fmt"
"path/filepath"
"bytes"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
const (
@@ -26,6 +22,12 @@ const (
RatchetSize = 256
RatchetExpiry = 2592000 // 30 days in seconds
TruncatedHashLen = 128 // bits
NameHashLength = 80 // bits
TokenOverhead = 16 // bytes
AESBlockSize = 16 // bytes
HashLength = 256 // bits
SigLength = KeySize // bits
HMACKeySize = 32 // bytes
)
type Identity struct {
@@ -36,351 +38,16 @@ type Identity struct {
ratchets map[string][]byte
ratchetExpiry map[string]int64
mutex sync.RWMutex
appData []byte
}
func New() (*Identity, error) {
i := &Identity{
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
}
// Generate X25519 key pair
var err error
i.privateKey = make([]byte, curve25519.ScalarSize)
if _, err = io.ReadFull(rand.Reader, i.privateKey); err != nil {
return nil, err
}
// Generate public key
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
if err != nil {
return nil, err
}
// Generate Ed25519 signing keypair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
i.signingKey = privateKey
i.verificationKey = publicKey
return i, nil
}
func FromBytes(data []byte) (*Identity, error) {
if len(data) != KeySize/8 {
return nil, errors.New("invalid key size")
}
i := &Identity{
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
}
// First 32 bytes are X25519 private key
i.privateKey = data[:32]
var err error
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
if err != nil {
return nil, err
}
// Next 32 bytes are Ed25519 private key
i.signingKey = ed25519.PrivateKey(data[32:])
i.verificationKey = i.signingKey.Public().(ed25519.PublicKey)
return i, nil
}
func (i *Identity) ToBytes() []byte {
data := make([]byte, KeySize/8)
copy(data[:32], i.privateKey)
copy(data[32:], i.signingKey)
return data
}
func (i *Identity) SaveToFile(path string) error {
return os.WriteFile(path, i.ToBytes(), 0600)
}
func LoadFromFile(path string) (*Identity, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return FromBytes(data)
}
func (i *Identity) Encrypt(plaintext []byte, ratchets []byte) ([]byte, error) {
// Generate ephemeral key pair
ephemeralPrivate := make([]byte, curve25519.ScalarSize)
if _, err := io.ReadFull(rand.Reader, ephemeralPrivate); err != nil {
return nil, err
}
ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint)
if err != nil {
return nil, err
}
// Perform key exchange
sharedSecret, err := curve25519.X25519(ephemeralPrivate, i.publicKey)
if err != nil {
return nil, err
}
// Generate AES key from shared secret using HKDF
hash := sha256.New
hkdf := hkdf.New(hash, sharedSecret, nil, nil)
aesKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf, aesKey); err != nil {
return nil, err
}
// Create AES-GCM cipher
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Generate nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt plaintext
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
// Combine ephemeral public key, nonce and ciphertext
result := make([]byte, len(ephemeralPublic)+len(nonce)+len(ciphertext))
copy(result, ephemeralPublic)
copy(result[len(ephemeralPublic):], nonce)
copy(result[len(ephemeralPublic)+len(nonce):], ciphertext)
return result, nil
}
func (i *Identity) Decrypt(ciphertext []byte, ratchets []byte) ([]byte, error) {
if len(ciphertext) <= curve25519.ScalarSize {
return nil, errors.New("invalid ciphertext")
}
// Extract ephemeral public key
ephemeralPublic := ciphertext[:curve25519.ScalarSize]
// Perform key exchange
sharedSecret, err := curve25519.X25519(i.privateKey, ephemeralPublic)
if err != nil {
return nil, err
}
// Generate AES key from shared secret using HKDF
hash := sha256.New
hkdf := hkdf.New(hash, sharedSecret, nil, nil)
aesKey := make([]byte, 32)
if _, err := io.ReadFull(hkdf, aesKey); err != nil {
return nil, err
}
// Create AES-GCM cipher
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Extract nonce and encrypted data
nonceSize := gcm.NonceSize()
if len(ciphertext) < curve25519.ScalarSize+nonceSize {
return nil, errors.New("invalid ciphertext")
}
nonce := ciphertext[curve25519.ScalarSize : curve25519.ScalarSize+nonceSize]
encryptedData := ciphertext[curve25519.ScalarSize+nonceSize:]
// Decrypt data
plaintext, err := gcm.Open(nil, nonce, encryptedData, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func (i *Identity) Sign(message []byte) []byte {
return ed25519.Sign(i.signingKey, message)
}
func (i *Identity) Verify(message, signature []byte) bool {
return ed25519.Verify(i.verificationKey, message, signature)
}
func (i *Identity) GetPublicKey() []byte {
return append([]byte{}, i.publicKey...)
}
func (i *Identity) AddRatchet(ratchetID string, ratchetKey []byte) {
i.mutex.Lock()
defer i.mutex.Unlock()
i.ratchets[ratchetID] = ratchetKey
i.ratchetExpiry[ratchetID] = time.Now().Unix() + RatchetExpiry
}
func (i *Identity) GetRatchet(ratchetID string) []byte {
i.mutex.RLock()
defer i.mutex.RUnlock()
if expiry, ok := i.ratchetExpiry[ratchetID]; ok {
if time.Now().Unix() < expiry {
return i.ratchets[ratchetID]
}
// Cleanup expired ratchet
delete(i.ratchets, ratchetID)
delete(i.ratchetExpiry, ratchetID)
}
return nil
}
// Helper functions
func TruncatedHash(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:TruncatedHashLen/8]
}
func FullHash(data []byte) []byte {
hash := sha256.Sum256(data)
return hash[:]
}
func HashFromHex(hexHash string) ([]byte, error) {
if len(hexHash) != TruncatedHashLen/4 { // hex string is twice the length of bytes
return nil, errors.New("invalid hash length")
}
hash := make([]byte, TruncatedHashLen/8)
_, err := hex.Decode(hash, []byte(hexHash))
if err != nil {
return nil, err
}
return hash, nil
}
func Recall(hash []byte) (*Identity, error) {
// Get config path from environment or default location
configDir := os.Getenv("RETICULUM_CONFIG_DIR")
if configDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
configDir = filepath.Join(homeDir, ".reticulum-go")
}
// Create identities directory if it doesn't exist
identitiesPath := filepath.Join(configDir, "identities")
if err := os.MkdirAll(identitiesPath, 0755); err != nil {
return nil, fmt.Errorf("failed to create identities directory: %w", err)
}
// Convert hash to hex for filename
hashHex := hex.EncodeToString(hash)
identityPath := filepath.Join(identitiesPath, hashHex)
// Check if identity file exists
if _, err := os.Stat(identityPath); os.IsNotExist(err) {
return nil, errors.New("identity not found")
}
// Load identity from file
identity, err := LoadFromFile(identityPath)
if err != nil {
return nil, fmt.Errorf("failed to load identity: %w", err)
}
// Verify the loaded identity matches the requested hash
if !bytes.Equal(TruncatedHash(identity.GetPublicKey()), hash) {
return nil, errors.New("identity hash mismatch")
}
return identity, nil
}
func LoadIdentity(cfg *common.ReticulumConfig) (*Identity, error) {
if cfg == nil {
return nil, errors.New("config cannot be nil")
}
// Try to load existing identity
identityPath := filepath.Join(filepath.Dir(cfg.ConfigPath), "identity")
if _, err := os.Stat(identityPath); err == nil {
// Identity exists, load it
return LoadFromFile(identityPath)
}
// Create new identity
identity, err := New()
if err != nil {
return nil, fmt.Errorf("failed to create new identity: %w", err)
}
// Save the new identity
if err := identity.SaveToFile(identityPath); err != nil {
return nil, fmt.Errorf("failed to save new identity: %w", err)
}
return identity, nil
}
func (i *Identity) GetCurrentRatchetKey() []byte {
i.mutex.RLock()
defer i.mutex.RUnlock()
// Generate new ratchet key if none exists
if len(i.ratchets) == 0 {
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return nil
}
ratchetID := fmt.Sprintf("%d", time.Now().Unix())
i.AddRatchet(ratchetID, key)
return key
}
// Return most recent ratchet key
var latestTime int64
var latestKey []byte
for id, key := range i.ratchets {
if expiry, ok := i.ratchetExpiry[id]; ok {
if expiry > latestTime {
latestTime = expiry
latestKey = key
}
}
}
return latestKey
}
func (i *Identity) EncryptSymmetric(plaintext []byte) ([]byte, error) {
key := i.GetCurrentRatchetKey()
if key == nil {
return nil, errors.New("no ratchet key available")
}
var (
knownDestinations = make(map[string][]interface{})
knownRatchets = make(map[string][]byte)
ratchetPersistLock sync.Mutex
)
func encryptAESGCM(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
@@ -396,15 +63,11 @@ func (i *Identity) EncryptSymmetric(plaintext []byte) ([]byte, error) {
return nil, err
}
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func (i *Identity) DecryptSymmetric(ciphertext []byte) ([]byte, error) {
key := i.GetCurrentRatchetKey()
if key == nil {
return nil, errors.New("no ratchet key available")
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decryptAESGCM(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
@@ -417,44 +80,264 @@ func (i *Identity) DecryptSymmetric(ciphertext []byte) ([]byte, error) {
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
return nil, err
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("decryption failed: %w", err)
return nil, err
}
return plaintext, nil
}
func (i *Identity) Hash() []byte {
return TruncatedHash(i.publicKey)
}
func (i *Identity) Hex() string {
return hex.EncodeToString(i.Hash())
}
func FromPublicKey(publicKey []byte) *Identity {
if len(publicKey) != curve25519.PointSize {
return nil
}
func New() (*Identity, error) {
i := &Identity{
publicKey: append([]byte{}, publicKey...),
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
}
// Generate Ed25519 verification key from the X25519 public key
hash := sha256.New()
hash.Write(publicKey)
seed := hash.Sum(nil)
// Generate X25519 key pair
i.privateKey = make([]byte, curve25519.ScalarSize)
if _, err := io.ReadFull(rand.Reader, i.privateKey); err != nil {
return nil, err
}
// Use the first 32 bytes of the hash as the verification key
i.verificationKey = ed25519.PublicKey(seed[:32])
var err error
i.publicKey, err = curve25519.X25519(i.privateKey, curve25519.Basepoint)
if err != nil {
return nil, err
}
// Generate Ed25519 signing keypair
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
i.signingKey = privKey
i.verificationKey = pubKey
return i, nil
}
func (i *Identity) GetPublicKey() []byte {
combined := make([]byte, KeySize/8)
copy(combined[:KeySize/16], i.publicKey)
copy(combined[KeySize/16:], i.verificationKey)
return combined
}
func (i *Identity) GetPrivateKey() []byte {
return append(i.privateKey, i.signingKey...)
}
func (i *Identity) Sign(data []byte) []byte {
return ed25519.Sign(i.signingKey, data)
}
func (i *Identity) Verify(data []byte, signature []byte) bool {
return ed25519.Verify(i.verificationKey, data, signature)
}
func (i *Identity) Encrypt(plaintext []byte, ratchet []byte) ([]byte, error) {
// Generate ephemeral key pair
ephemeralPrivate := make([]byte, curve25519.ScalarSize)
if _, err := io.ReadFull(rand.Reader, ephemeralPrivate); err != nil {
return nil, err
}
ephemeralPublic, err := curve25519.X25519(ephemeralPrivate, curve25519.Basepoint)
if err != nil {
return nil, err
}
var targetKey []byte
if ratchet != nil {
targetKey = ratchet
} else {
targetKey = i.publicKey
}
sharedSecret, err := curve25519.X25519(ephemeralPrivate, targetKey)
if err != nil {
return nil, err
}
// Generate encryption key using HKDF
hkdf := hkdf.New(sha256.New, sharedSecret, i.Hash(), nil)
key := make([]byte, 32)
if _, err := io.ReadFull(hkdf, key); err != nil {
return nil, err
}
// Encrypt using AES-GCM
ciphertext, err := encryptAESGCM(key, plaintext)
if err != nil {
return nil, err
}
return append(ephemeralPublic, ciphertext...), nil
}
func (i *Identity) Hash() []byte {
h := sha256.New()
h.Write(i.GetPublicKey())
return h.Sum(nil)
}
func TruncatedHash(data []byte) []byte {
h := sha256.New()
h.Write(data)
fullHash := h.Sum(nil)
return fullHash[:TruncatedHashLen/8]
}
func GetRandomHash() []byte {
randomData := make([]byte, TruncatedHashLen/8)
rand.Read(randomData)
return TruncatedHash(randomData)
}
func Remember(packetHash, destHash []byte, publicKey []byte, appData []byte) {
knownDestinations[string(destHash)] = []interface{}{
time.Now().Unix(),
packetHash,
publicKey,
appData,
}
}
func ValidateAnnounce(packet []byte, destHash []byte, publicKey []byte, signature []byte, appData []byte) bool {
if len(publicKey) != KeySize/8 {
return false
}
announced := &Identity{}
announced.publicKey = publicKey[:KeySize/16]
announced.verificationKey = publicKey[KeySize/16:]
signedData := append(destHash, publicKey...)
signedData = append(signedData, appData...)
if !announced.Verify(signedData, signature) {
return false
}
Remember(packet, destHash, publicKey, appData)
return true
}
func FromPublicKey(publicKey []byte) *Identity {
if len(publicKey) != KeySize/8 {
return nil
}
i := &Identity{
publicKey: publicKey[:KeySize/16],
verificationKey: publicKey[KeySize/16:],
ratchets: make(map[string][]byte),
ratchetExpiry: make(map[string]int64),
}
return i
}
func (i *Identity) Hex() string {
return fmt.Sprintf("%x", i.Hash())
}
func (i *Identity) String() string {
return i.Hex()
}
func Recall(hash []byte) (*Identity, error) {
// TODO: Implement persistence
// For now just create new identity
return New()
}
func (i *Identity) GenerateHMACKey() []byte {
hmacKey := make([]byte, HMACKeySize)
if _, err := io.ReadFull(rand.Reader, hmacKey); err != nil {
return nil
}
return hmacKey
}
func (i *Identity) ComputeHMAC(key, message []byte) []byte {
h := hmac.New(sha256.New, key)
h.Write(message)
return h.Sum(nil)
}
func (i *Identity) ValidateHMAC(key, message, messageHMAC []byte) bool {
expectedHMAC := i.ComputeHMAC(key, message)
return hmac.Equal(messageHMAC, expectedHMAC)
}
func (i *Identity) GetCurrentRatchetKey() []byte {
i.mutex.RLock()
defer i.mutex.RUnlock()
// Generate new ratchet key if none exists
if len(i.ratchets) == 0 {
key := make([]byte, RatchetSize/8)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil
}
i.ratchets[string(key)] = key
i.ratchetExpiry[string(key)] = time.Now().Unix() + RatchetExpiry
return key
}
// Return most recent ratchet key
var latestKey []byte
var latestTime int64
for key, expiry := range i.ratchetExpiry {
if expiry > latestTime {
latestTime = expiry
latestKey = i.ratchets[key]
}
}
return latestKey
}
func (i *Identity) EncryptSymmetric(plaintext []byte, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, errors.New("invalid key length")
}
return encryptAESGCM(key, plaintext)
}
func (i *Identity) DecryptSymmetric(ciphertext []byte, key []byte) ([]byte, error) {
if len(key) != 32 {
return nil, errors.New("invalid key length")
}
return decryptAESGCM(key, ciphertext)
}
func (i *Identity) Decrypt(ciphertext []byte) ([]byte, error) {
if len(ciphertext) < curve25519.PointSize {
return nil, errors.New("ciphertext too short")
}
ephemeralPublic := ciphertext[:curve25519.PointSize]
encryptedData := ciphertext[curve25519.PointSize:]
// Compute shared secret
sharedSecret, err := curve25519.X25519(i.privateKey, ephemeralPublic)
if err != nil {
return nil, err
}
// Derive key using HKDF
hkdf := hkdf.New(sha256.New, sharedSecret, i.Hash(), nil)
key := make([]byte, 32)
if _, err := io.ReadFull(hkdf, key); err != nil {
return nil, err
}
// Decrypt data
return decryptAESGCM(key, encryptedData)
}

View File

@@ -1,10 +1,11 @@
package interfaces
import (
"fmt"
"time"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
@@ -15,15 +16,27 @@ const (
)
type Interface interface {
common.NetworkInterface
Send(data []byte, target string) error
Detach()
IsEnabled() bool
GetName() string
GetType() common.InterfaceType
GetMode() common.InterfaceMode
IsOnline() bool
IsDetached() bool
Detach()
Send(data []byte, addr string) error
SetPacketCallback(common.PacketCallback)
GetPacketCallback() common.PacketCallback
}
type BaseInterface struct {
common.BaseInterface
name string
mode common.InterfaceMode
ifType common.InterfaceType
online bool
detached bool
mtu int
mutex sync.RWMutex
packetCallback common.PacketCallback
}
func (i *BaseInterface) SetPacketCallback(callback common.PacketCallback) {

View File

@@ -5,6 +5,7 @@ import (
"net"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
@@ -41,17 +42,19 @@ type TCPClientInterface struct {
packetBuffer []byte
packetType byte
packetCallback common.PacketCallback
mutex sync.RWMutex
detached bool
}
func NewTCPClient(name string, targetAddr string, targetPort int, kissFraming bool, i2pTunneled bool) (*TCPClientInterface, error) {
tc := &TCPClientInterface{
BaseInterface: BaseInterface{
BaseInterface: common.BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
MTU: 1064,
Bitrate: 10000000, // 10Mbps estimate
},
name: name,
mode: common.IF_MODE_FULL,
ifType: common.IF_TYPE_TCP,
online: false,
mtu: 1064,
detached: false,
},
targetAddr: targetAddr,
targetPort: targetPort,
@@ -285,139 +288,62 @@ func (tc *TCPClientInterface) GetName() string {
return tc.Name
}
func (tc *TCPClientInterface) GetPacketCallback() common.PacketCallback {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.packetCallback
}
func (tc *TCPClientInterface) IsDetached() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.detached
}
func (tc *TCPClientInterface) IsOnline() bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
return tc.online
}
type TCPServerInterface struct {
BaseInterface
server net.Listener
listener net.Listener
connections map[string]net.Conn
mutex sync.RWMutex
bindAddr string
bindPort int
preferIPv6 bool
spawned bool
port int
host string
kissFraming bool
i2pTunneled bool
spawned []*TCPClientInterface
spawnedMutex sync.RWMutex
packetCallback func([]byte, interface{})
packetCallback common.PacketCallback
detached bool
}
func NewTCPServer(name string, bindAddr string, bindPort int, preferIPv6 bool, i2pTunneled bool) (*TCPServerInterface, error) {
func NewTCPServer(name string, bindAddr string, bindPort int, kissFraming bool, i2pTunneled bool, preferIPv6 bool) (*TCPServerInterface, error) {
ts := &TCPServerInterface{
BaseInterface: BaseInterface{
BaseInterface: common.BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_TCP,
MTU: 1064,
Bitrate: 10000000,
},
name: name,
mode: common.IF_MODE_FULL,
ifType: common.IF_TYPE_TCP,
online: false,
mtu: common.DEFAULT_MTU,
detached: false,
},
connections: make(map[string]net.Conn),
bindAddr: bindAddr,
bindPort: bindPort,
preferIPv6: preferIPv6,
kissFraming: kissFraming,
i2pTunneled: i2pTunneled,
spawned: make([]*TCPClientInterface, 0),
}
// Resolve bind address
var addr string
if ts.bindAddr == "" {
if ts.preferIPv6 {
addr = fmt.Sprintf("[::0]:%d", ts.bindPort)
} else {
addr = fmt.Sprintf("0.0.0.0:%d", ts.bindPort)
}
} else {
addr = fmt.Sprintf("%s:%d", ts.bindAddr, ts.bindPort)
}
// Create listener
var err error
ts.server, err = net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
}
ts.Online = true
ts.IN = true
// Start accept loop
go ts.acceptLoop()
return ts, nil
}
func (ts *TCPServerInterface) acceptLoop() {
for {
conn, err := ts.server.Accept()
if err != nil {
if !ts.Detached {
continue
}
return
}
// Create new client interface for this connection
client := &TCPClientInterface{
BaseInterface: BaseInterface{
BaseInterface: common.BaseInterface{
Name: fmt.Sprintf("Client-%s-%s", ts.Name, conn.RemoteAddr()),
Mode: ts.Mode,
Type: common.IF_TYPE_TCP,
MTU: ts.MTU,
Bitrate: ts.Bitrate,
},
},
conn: conn,
i2pTunneled: ts.i2pTunneled,
}
// Configure TCP options
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetNoDelay(true)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(time.Duration(TCP_PROBE_INTERVAL) * time.Second)
}
client.Online = true
client.IN = ts.IN
client.OUT = ts.OUT
// Add to spawned interfaces
ts.spawnedMutex.Lock()
ts.spawned = append(ts.spawned, client)
ts.spawnedMutex.Unlock()
// Start client read loop
go client.readLoop()
}
}
func (ts *TCPServerInterface) Detach() {
ts.BaseInterface.Detach()
if ts.server != nil {
ts.server.Close()
}
ts.spawnedMutex.Lock()
for _, client := range ts.spawned {
client.Detach()
}
ts.spawned = nil
ts.spawnedMutex.Unlock()
}
func (ts *TCPServerInterface) ProcessOutgoing(data []byte) error {
ts.spawnedMutex.RLock()
defer ts.spawnedMutex.RUnlock()
var lastErr error
for _, client := range ts.spawned {
if err := client.ProcessOutgoing(data); err != nil {
lastErr = err
}
}
return lastErr
}
func (ts *TCPServerInterface) String() string {
addr := ts.bindAddr
if addr == "" {
@@ -427,17 +353,37 @@ func (ts *TCPServerInterface) String() string {
addr = "0.0.0.0"
}
}
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.Name, addr, ts.bindPort)
return fmt.Sprintf("TCPServerInterface[%s/%s:%d]", ts.name, addr, ts.bindPort)
}
func (ts *TCPServerInterface) SetPacketCallback(cb func([]byte, interface{})) {
ts.packetCallback = cb
func (ts *TCPServerInterface) SetPacketCallback(callback common.PacketCallback) {
ts.mutex.Lock()
defer ts.mutex.Unlock()
ts.packetCallback = callback
}
func (ts *TCPServerInterface) GetPacketCallback() common.PacketCallback {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.packetCallback
}
func (ts *TCPServerInterface) IsEnabled() bool {
return ts.Online
return ts.online
}
func (ts *TCPServerInterface) GetName() string {
return ts.Name
return ts.name
}
func (ts *TCPServerInterface) IsDetached() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.detached
}
func (ts *TCPServerInterface) IsOnline() bool {
ts.mutex.RLock()
defer ts.mutex.RUnlock()
return ts.online
}

View File

@@ -3,95 +3,139 @@ package interfaces
import (
"fmt"
"net"
"sync"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
)
type UDPInterface struct {
BaseInterface
conn *net.UDPConn
listenAddr *net.UDPAddr
addr *net.UDPAddr
targetAddr *net.UDPAddr
mutex sync.RWMutex
readBuffer []byte
txBytes uint64
rxBytes uint64
mtu int
bitrate int
}
func NewUDPInterface(name string, addr string, target string) (*UDPInterface, error) {
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
var targetAddr *net.UDPAddr
if target != "" {
targetAddr, err = net.ResolveUDPAddr("udp", target)
if err != nil {
return nil, err
}
}
func NewUDPInterface(name string, listenAddr string, targetAddr string) (*UDPInterface, error) {
ui := &UDPInterface{
BaseInterface: BaseInterface{
BaseInterface: common.BaseInterface{
Name: name,
Mode: common.IF_MODE_FULL,
Type: common.IF_TYPE_UDP,
MTU: 1500,
Bitrate: 100000000, // 100Mbps estimate
name: name,
mode: common.IF_MODE_FULL,
ifType: common.IF_TYPE_UDP,
online: false,
mtu: common.DEFAULT_MTU,
detached: false,
},
},
readBuffer: make([]byte, 65535),
addr: udpAddr,
targetAddr: targetAddr,
readBuffer: make([]byte, common.DEFAULT_MTU),
}
// Parse listen address
laddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return nil, fmt.Errorf("invalid listen address: %v", err)
}
ui.listenAddr = laddr
// Parse target address if provided
if targetAddr != "" {
taddr, err := net.ResolveUDPAddr("udp", targetAddr)
if err != nil {
return nil, fmt.Errorf("invalid target address: %v", err)
}
ui.targetAddr = taddr
ui.BaseInterface.OUT = true
}
// Create UDP connection
conn, err := net.ListenUDP("udp", ui.listenAddr)
if err != nil {
return nil, fmt.Errorf("failed to listen on UDP: %v", err)
}
ui.conn = conn
ui.BaseInterface.IN = true
ui.BaseInterface.Online = true
// Start read loop
go ui.readLoop()
return ui, nil
}
func (ui *UDPInterface) readLoop() {
for {
if !ui.BaseInterface.Online {
return
func (ui *UDPInterface) GetName() string {
return ui.name
}
n, remoteAddr, err := ui.conn.ReadFromUDP(ui.readBuffer)
func (ui *UDPInterface) GetType() common.InterfaceType {
return ui.ifType
}
func (ui *UDPInterface) GetMode() common.InterfaceMode {
return ui.mode
}
func (ui *UDPInterface) IsOnline() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.online
}
func (ui *UDPInterface) IsDetached() bool {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.detached
}
func (ui *UDPInterface) Detach() {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.detached = true
if ui.conn != nil {
ui.conn.Close()
}
}
func (ui *UDPInterface) Send(data []byte, addr string) error {
if !ui.IsOnline() {
return fmt.Errorf("interface offline")
}
targetAddr := ui.targetAddr
if addr != "" {
var err error
targetAddr, err = net.ResolveUDPAddr("udp", addr)
if err != nil {
if !ui.BaseInterface.Detached {
continue
return fmt.Errorf("invalid target address: %v", err)
}
return
}
// If no target address is set, use the first sender's address
if ui.targetAddr == nil {
ui.targetAddr = remoteAddr
ui.BaseInterface.OUT = true
if targetAddr == nil {
return fmt.Errorf("no target address configured")
}
// Copy received data
data := make([]byte, n)
copy(data, ui.readBuffer[:n])
_, err := ui.conn.WriteToUDP(data, targetAddr)
if err != nil {
return fmt.Errorf("UDP write failed: %v", err)
}
// Process packet
ui.ProcessIncoming(data)
return nil
}
func (ui *UDPInterface) SetPacketCallback(callback common.PacketCallback) {
ui.mutex.Lock()
defer ui.mutex.Unlock()
ui.packetCallback = callback
}
func (ui *UDPInterface) GetPacketCallback() common.PacketCallback {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.packetCallback
}
func (ui *UDPInterface) ProcessIncoming(data []byte) {
if callback := ui.GetPacketCallback(); callback != nil {
callback(data, ui)
}
}
func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
if !ui.BaseInterface.Online || ui.targetAddr == nil {
return fmt.Errorf("interface offline or no target address configured")
if !ui.IsOnline() {
return fmt.Errorf("interface offline")
}
if ui.targetAddr == nil {
return fmt.Errorf("no target address configured")
}
_, err := ui.conn.WriteToUDP(data, ui.targetAddr)
@@ -99,17 +143,33 @@ func (ui *UDPInterface) ProcessOutgoing(data []byte) error {
return fmt.Errorf("UDP write failed: %v", err)
}
ui.BaseInterface.ProcessOutgoing(data)
return nil
}
ui.mutex.Lock()
ui.txBytes += uint64(len(data))
ui.mutex.Unlock()
func (ui *UDPInterface) Detach() {
ui.BaseInterface.Detach()
if ui.conn != nil {
ui.conn.Close()
}
return nil
}
func (ui *UDPInterface) GetConn() net.Conn {
return ui.conn
}
func (ui *UDPInterface) GetTxBytes() uint64 {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.txBytes
}
func (ui *UDPInterface) GetRxBytes() uint64 {
ui.mutex.RLock()
defer ui.mutex.RUnlock()
return ui.rxBytes
}
func (ui *UDPInterface) GetMTU() int {
return ui.mtu
}
func (ui *UDPInterface) GetBitrate() int {
return ui.bitrate
}

View File

@@ -6,14 +6,15 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/destination"
"github.com/Sudo-Ivan/reticulum-go/pkg/identity"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
"github.com/Sudo-Ivan/reticulum-go/pkg/transport"
)
const (
@@ -33,11 +34,16 @@ const (
STATUS_ACTIVE = 0x01
STATUS_CLOSED = 0x02
STATUS_FAILED = 0x03
// Add packet types
PACKET_TYPE_DATA = 0x00
PACKET_TYPE_LINK = 0x01
PACKET_TYPE_IDENTIFY = 0x02
)
type Link struct {
mutex sync.RWMutex
destination interface{}
destination *destination.Destination
status byte
establishedAt time.Time
lastInbound time.Time
@@ -52,57 +58,87 @@ type Link struct {
rtt float64
establishmentRate float64
trackPhyStats bool
rssi float64
snr float64
q float64
resourceStrategy byte
establishedCallback func(*Link)
closedCallback func(*Link)
packetCallback func([]byte, *packet.Packet)
identifiedCallback func(*Link, *identity.Identity)
teardownReason byte
hmacKey []byte
transport *transport.Transport
// Add missing fields
rssi float64
snr float64
q float64
resourceCallback func(interface{}) bool
resourceStartedCallback func(interface{})
resourceConcludedCallback func(interface{})
remoteIdentifiedCallback func(*Link, *identity.Identity)
resourceStrategy byte
}
func New(dest interface{}, establishedCb func(*Link), closedCb func(*Link)) *Link {
l := &Link{
func NewLink(dest *destination.Destination, transport *transport.Transport, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
return &Link{
destination: dest,
status: STATUS_PENDING,
establishedAt: time.Time{},
transport: transport,
establishedCallback: establishedCallback,
closedCallback: closedCallback,
establishedAt: time.Time{}, // Zero time until established
lastInbound: time.Time{},
lastOutbound: time.Time{},
lastDataReceived: time.Time{},
lastDataSent: time.Time{},
resourceStrategy: ACCEPT_NONE,
establishedCallback: establishedCb,
closedCallback: closedCb,
}
}
return l
}
func (l *Link) Identify(id *identity.Identity) error {
func (l *Link) Establish() error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_ACTIVE {
return errors.New("link not active")
if l.status != STATUS_PENDING {
return errors.New("link already established or failed")
}
// Create identification message
idMsg := append(id.GetPublicKey(), id.Sign(l.linkID)...)
destPublicKey := l.destination.GetPublicKey()
if destPublicKey == nil {
return errors.New("destination has no public key")
}
// Encrypt and send identification
err := l.SendPacket(idMsg)
// Create link request packet
p, err := packet.NewPacket(
packet.PACKET_TYPE_LINK,
0x00, // flags
0x00, // hops
destPublicKey,
l.linkID,
)
if err != nil {
return err
}
return nil
// Send through transport
return l.transport.SendPacket(p)
}
func (l *Link) Identify(id *identity.Identity) error {
if !l.IsActive() {
return errors.New("link not active")
}
// Create identify packet
p, err := packet.NewPacket(
packet.PACKET_TYPE_IDENTIFY,
0x00,
0x00,
l.destination.GetPublicKey(),
id.GetPublicKey(),
)
if err != nil {
return err
}
return l.transport.SendPacket(p)
}
func (l *Link) HandleIdentification(data []byte) error {
@@ -110,26 +146,27 @@ func (l *Link) HandleIdentification(data []byte) error {
defer l.mutex.Unlock()
if len(data) < ed25519.PublicKeySize+ed25519.SignatureSize {
return errors.New("invalid identification data")
return errors.New("invalid identification data length")
}
pubKey := data[:ed25519.PublicKeySize]
signature := data[ed25519.PublicKeySize:]
remoteIdentity := &identity.Identity{}
if !remoteIdentity.LoadPublicKey(pubKey) {
return errors.New("invalid remote public key")
remoteIdentity := identity.FromPublicKey(pubKey)
if remoteIdentity == nil {
return errors.New("invalid remote identity")
}
// Verify signature of link ID
if !remoteIdentity.Verify(l.linkID, signature) {
return errors.New("invalid identification signature")
// Verify signature
signData := append(l.linkID, pubKey...)
if !remoteIdentity.Verify(signData, signature) {
return errors.New("invalid signature")
}
l.remoteIdentity = remoteIdentity
if l.remoteIdentifiedCallback != nil {
l.remoteIdentifiedCallback(l, remoteIdentity)
if l.identifiedCallback != nil {
l.identifiedCallback(l, remoteIdentity)
}
return nil
@@ -227,10 +264,13 @@ func (r *RequestReceipt) Concluded() bool {
return status == STATUS_ACTIVE || status == STATUS_FAILED
}
func (l *Link) TrackPhyStats(track bool) {
func (l *Link) TrackPhyStats(rssi float64, snr float64, q float64) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.trackPhyStats = track
l.rssi = rssi
l.snr = snr
l.q = q
}
func (l *Link) GetRSSI() float64 {
@@ -361,7 +401,7 @@ func (l *Link) SetResourceConcludedCallback(callback func(interface{})) {
func (l *Link) SetRemoteIdentifiedCallback(callback func(*Link, *identity.Identity)) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.remoteIdentifiedCallback = callback
l.identifiedCallback = callback
}
func (l *Link) SetResourceStrategy(strategy byte) error {
@@ -375,49 +415,6 @@ func (l *Link) SetResourceStrategy(strategy byte) error {
return nil
}
func NewLink(destination interface{}, establishedCallback func(*Link), closedCallback func(*Link)) *Link {
l := &Link{
destination: destination,
status: STATUS_PENDING,
establishedAt: time.Time{},
lastInbound: time.Time{},
lastOutbound: time.Time{},
lastDataReceived: time.Time{},
lastDataSent: time.Time{},
establishedCallback: establishedCallback,
closedCallback: closedCallback,
resourceStrategy: ACCEPT_NONE,
trackPhyStats: false,
}
return l
}
func (l *Link) Establish() error {
l.mutex.Lock()
defer l.mutex.Unlock()
if l.status != STATUS_PENDING {
return errors.New("link already established or failed")
}
// Generate session key using ECDH
ephemeralKey := make([]byte, 32)
if _, err := rand.Read(ephemeralKey); err != nil {
return err
}
l.sessionKey = ephemeralKey
l.establishedAt = time.Now()
l.status = STATUS_ACTIVE
if l.establishedCallback != nil {
l.establishedCallback(l)
}
return nil
}
func (l *Link) SendPacket(data []byte) error {
l.mutex.Lock()
defer l.mutex.Unlock()
@@ -426,8 +423,14 @@ func (l *Link) SendPacket(data []byte) error {
return errors.New("link not active")
}
// Encrypt data using session key
encryptedData, err := l.encrypt(data)
// Compute HMAC first
messageHMAC := l.destination.GetIdentity().ComputeHMAC(l.hmacKey, data)
// Combine data and HMAC
authenticatedData := append(data, messageHMAC...)
// Encrypt authenticated data using session key
encryptedData, err := l.encrypt(authenticatedData)
if err != nil {
return err
}
@@ -456,11 +459,24 @@ func (l *Link) HandleInbound(data []byte) error {
return err
}
// Split message and HMAC
if len(decryptedData) < sha256.Size {
return errors.New("received data too short")
}
message := decryptedData[:len(decryptedData)-sha256.Size]
messageHMAC := decryptedData[len(decryptedData)-sha256.Size:]
// Verify HMAC
if !l.destination.GetIdentity().ValidateHMAC(l.hmacKey, message, messageHMAC) {
return errors.New("invalid message authentication code")
}
l.lastInbound = time.Now()
l.lastDataReceived = time.Now()
if l.packetCallback != nil {
l.packetCallback(decryptedData, nil)
l.packetCallback(message, nil)
}
return nil
@@ -514,16 +530,7 @@ func (l *Link) decrypt(data []byte) ([]byte, error) {
}
func (l *Link) UpdatePhyStats(rssi float64, snr float64, q float64) {
if !l.trackPhyStats {
return
}
l.mutex.Lock()
defer l.mutex.Unlock()
l.rssi = rssi
l.snr = snr
l.q = q
l.TrackPhyStats(rssi, snr, q)
}
func (l *Link) GetRTT() float64 {

View File

@@ -1,64 +1,110 @@
package packet
import (
"crypto/rand"
"encoding/binary"
"errors"
)
const (
// Packet Types
PacketTypeData = 0x00
PacketTypeAnnounce = 0x01
PacketTypeLink = 0x02
PacketTypeProof = 0x03
PACKET_TYPE_DATA = 0x00
PACKET_TYPE_LINK = 0x01
PACKET_TYPE_IDENTIFY = 0x02
// Sizes
HeaderSize = 2
AddressSize = 16
ContextSize = 1
MaxDataSize = 465 // Maximum size of payload data
MaxDataSize = 465
RandomBlobSize = 16
)
// Header flags and types
const (
// First byte flags
IFACFlag = 0x80 // Interface authentication code flag
HeaderTypeFlag = 0x40 // Header type flag
ContextFlag = 0x20 // Context flag
PropagationFlags = 0x18 // Propagation type flags (bits 3-4)
DestinationFlags = 0x06 // Destination type flags (bits 1-2)
PacketTypeFlags = 0x01 // Packet type flags (bit 0)
IFACFlag = 0x80
HeaderTypeFlag = 0x40
ContextFlag = 0x20
PropagationFlags = 0x18
DestinationFlags = 0x06
PacketTypeFlags = 0x01
// Second byte
HopsField = 0xFF // Number of hops (entire byte)
HopsField = 0xFF
)
type Packet struct {
Header [2]byte
Addresses []byte // Either 16 or 32 bytes depending on header type
Addresses []byte
Context byte
Data []byte
AccessCode []byte // Optional: Only present if IFAC flag is set
AccessCode []byte
RandomBlob []byte
}
func NewPacket(headerType, propagationType, destinationType, packetType byte, hops byte) *Packet {
func NewAnnouncePacket(destHash []byte, publicKey []byte, appData []byte) (*Packet, error) {
p := &Packet{
Header: [2]byte{0, hops},
Addresses: make([]byte, 0),
Data: make([]byte, 0),
Header: [2]byte{0, 0}, // Start with 0 hops
Addresses: make([]byte, AddressSize),
Data: make([]byte, 0, MaxDataSize),
}
// Set header type
if headerType == HeaderType2 {
p.Header[0] |= HeaderTypeFlag
p.Addresses = make([]byte, 2*AddressSize) // Two address fields
} else {
p.Addresses = make([]byte, AddressSize) // One address field
// Set header flags for announce packet
p.Header[0] |= HeaderTypeFlag // Single address
p.Header[0] |= (PropagationBroadcast << 3) & PropagationFlags // Broadcast
p.Header[0] |= (DestinationSingle << 1) & DestinationFlags // Single destination
p.Header[0] |= PacketTypeAnnounce & PacketTypeFlags // Announce type
// Set destination hash
if len(destHash) != AddressSize {
return nil, errors.New("invalid destination hash size")
}
copy(p.Addresses, destHash)
// Build announce data
// Public key
p.Data = append(p.Data, publicKey...)
// App data length and content
appDataLen := make([]byte, 2)
binary.BigEndian.PutUint16(appDataLen, uint16(len(appData)))
p.Data = append(p.Data, appDataLen...)
p.Data = append(p.Data, appData...)
// Add random blob
randomBlob := make([]byte, RandomBlobSize)
if _, err := rand.Read(randomBlob); err != nil {
return nil, err
}
p.RandomBlob = randomBlob
p.Data = append(p.Data, randomBlob...)
return p, nil
}
// Set propagation type
p.Header[0] |= (propagationType << 3) & PropagationFlags
func NewPacket(packetType byte, flags byte, hops byte, destKey []byte, data []byte) (*Packet, error) {
if len(destKey) != AddressSize {
return nil, errors.New("invalid destination key length")
}
// Set destination type
p.Header[0] |= (destinationType << 1) & DestinationFlags
p := &Packet{
Header: [2]byte{flags, hops},
Addresses: make([]byte, AddressSize),
Data: data,
}
// Set packet type
// Set packet type in flags
p.Header[0] |= packetType & PacketTypeFlags
return p
// Copy destination address
copy(p.Addresses, destKey)
return p, nil
}
func (p *Packet) SetAccessCode(code []byte) {

View File

@@ -2,13 +2,12 @@ package resource
import (
"crypto/sha256"
"encoding/binary"
"errors"
"io"
"path/filepath"
"strings"
"sync"
"time"
"path/filepath"
)
const (
@@ -65,6 +64,7 @@ type Resource struct {
mutex sync.RWMutex
data []byte
fileHandle io.ReadWriteSeeker
fileName string
hash []byte
randomHash []byte
originalHash []byte
@@ -118,6 +118,10 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
if err != nil {
return nil, err
}
if namer, ok := v.(interface{ Name() string }); ok {
r.fileName = namer.Name()
}
default:
return nil, errors.New("unsupported data type")
}
@@ -138,7 +142,7 @@ func New(data interface{}, autoCompress bool) (*Resource, error) {
r.transferSize = int64(float64(r.dataSize) * compressibility)
} else if r.fileHandle != nil {
// For file handles, use extension-based estimation
ext := strings.ToLower(filepath.Ext(r.fileHandle.Name()))
ext := strings.ToLower(filepath.Ext(r.fileName))
r.transferSize = estimateFileCompression(r.dataSize, ext)
}

View File

@@ -3,11 +3,13 @@ package transport
import (
"encoding/binary"
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/Sudo-Ivan/reticulum-go/pkg/common"
"github.com/Sudo-Ivan/reticulum-go/pkg/packet"
)
var (
@@ -603,3 +605,28 @@ func (t *Transport) findLink(dest []byte) *Link {
// in the Transport struct for better performance
return nil
}
func (t *Transport) SendPacket(p *packet.Packet) error {
t.mutex.RLock()
defer t.mutex.RUnlock()
// Serialize packet
data, err := p.Serialize()
if err != nil {
return fmt.Errorf("failed to serialize packet: %w", err)
}
// Find appropriate interface
destHash := p.Addresses[:packet.AddressSize]
path, exists := t.paths[string(destHash)]
if !exists {
return errors.New("no path to destination")
}
// Send through interface
if err := path.Interface.Send(data, ""); err != nil {
return fmt.Errorf("failed to send packet: %w", err)
}
return nil
}