mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
434 lines
15 KiB
Go
434 lines
15 KiB
Go
package decoder
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
pb "meshstream/proto/generated/meshtastic"
|
|
)
|
|
|
|
// DefaultPrivateKey is the key used by pseudo public channels
|
|
const DefaultPrivateKey = "AQ=="
|
|
|
|
// ChannelKeys maps channelId to privateKey
|
|
var ChannelKeys = make(map[string][]byte)
|
|
var channelKeysMutex sync.RWMutex
|
|
|
|
// AddChannelKey adds a new channel key to the map
|
|
func AddChannelKey(channelId, base64Key string) error {
|
|
key, err := base64.StdEncoding.DecodeString(base64Key)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid base64 key: %v", err)
|
|
}
|
|
|
|
// Ensure the key is properly padded to be a valid AES key length (16, 24, or 32 bytes)
|
|
key = PadKey(key)
|
|
|
|
channelKeysMutex.Lock()
|
|
defer channelKeysMutex.Unlock()
|
|
ChannelKeys[channelId] = key
|
|
return nil
|
|
}
|
|
|
|
// GetChannelKey retrieves a channel key from the map, or returns the default key if not found
|
|
func GetChannelKey(channelId string) []byte {
|
|
channelKeysMutex.RLock()
|
|
defer channelKeysMutex.RUnlock()
|
|
|
|
if key, ok := ChannelKeys[channelId]; ok {
|
|
return key
|
|
}
|
|
|
|
// Return the default key if no specific key is found
|
|
defaultKey, _ := base64.StdEncoding.DecodeString(DefaultPrivateKey)
|
|
return PadKey(defaultKey)
|
|
}
|
|
|
|
// PadKey ensures the key is properly padded to be a valid AES key length (16, 24, or 32 bytes)
|
|
func PadKey(key []byte) []byte {
|
|
// If key length is already valid, return as is
|
|
if len(key) == 16 || len(key) == 24 || len(key) == 32 {
|
|
return key
|
|
}
|
|
|
|
// Pad to the next valid AES key length
|
|
if len(key) < 16 {
|
|
paddedKey := make([]byte, 16)
|
|
copy(paddedKey, key)
|
|
return paddedKey
|
|
} else if len(key) < 24 {
|
|
paddedKey := make([]byte, 24)
|
|
copy(paddedKey, key)
|
|
return paddedKey
|
|
} else {
|
|
paddedKey := make([]byte, 32)
|
|
copy(paddedKey, key)
|
|
return paddedKey
|
|
}
|
|
}
|
|
|
|
// TopicInfo contains parsed information about a Meshtastic MQTT topic
|
|
type TopicInfo struct {
|
|
FullTopic string
|
|
RegionPath string
|
|
Version string
|
|
Format string
|
|
Channel string
|
|
UserID string
|
|
}
|
|
|
|
// ParseTopic parses a Meshtastic MQTT topic into its components
|
|
func ParseTopic(topic string) (*TopicInfo, error) {
|
|
info := &TopicInfo{
|
|
FullTopic: topic,
|
|
}
|
|
|
|
// Format: msh/REGION_PATH/VERSION/FORMAT/CHANNELNAME/USERID
|
|
// Example: msh/US/CA/Motherlode/2/e/LongFast/!abcd1234
|
|
// Example: msh/US/CA/Motherlode/2/json/LongFast/!abcd1234
|
|
parts := strings.Split(topic, "/")
|
|
if len(parts) < 4 {
|
|
return info, fmt.Errorf("invalid topic format: %s", topic)
|
|
}
|
|
|
|
// Find protocol version and format indices by looking for "2" followed by "e" or "json"
|
|
versionIndex := -1
|
|
formatIndex := -1
|
|
|
|
for i := 1; i < len(parts)-1; i++ {
|
|
if parts[i] == "2" {
|
|
// Found the version
|
|
versionIndex = i
|
|
formatIndex = i + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
if versionIndex == -1 || formatIndex >= len(parts) {
|
|
// Could not find proper version/format markers
|
|
return info, fmt.Errorf("invalid topic format, missing version/format: %s", topic)
|
|
}
|
|
|
|
// Extract region path (all segments between "msh" and version)
|
|
if versionIndex > 1 {
|
|
info.RegionPath = strings.Join(parts[1:versionIndex], "/")
|
|
}
|
|
|
|
// Extract version and format
|
|
info.Version = parts[versionIndex]
|
|
info.Format = parts[formatIndex]
|
|
|
|
// Extract channel and user ID
|
|
channelIndex := formatIndex + 1
|
|
userIdIndex := channelIndex + 1
|
|
|
|
if channelIndex < len(parts) {
|
|
info.Channel = parts[channelIndex]
|
|
}
|
|
|
|
if userIdIndex < len(parts) {
|
|
info.UserID = parts[userIdIndex]
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// DecodeEncodedMessage decodes a binary encoded message (format "e")
|
|
func DecodeEncodedMessage(payload []byte) (*pb.ServiceEnvelope, error) {
|
|
var serviceEnvelope pb.ServiceEnvelope
|
|
if err := proto.Unmarshal(payload, &serviceEnvelope); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal ServiceEnvelope: %v", err)
|
|
}
|
|
return &serviceEnvelope, nil
|
|
}
|
|
|
|
// DecodeJSONMessage decodes a JSON message (format "json")
|
|
func DecodeJSONMessage(payload []byte) (map[string]interface{}, error) {
|
|
var jsonData map[string]interface{}
|
|
if err := json.Unmarshal(payload, &jsonData); err != nil {
|
|
return nil, fmt.Errorf("failed to parse JSON: %v", err)
|
|
}
|
|
return jsonData, nil
|
|
}
|
|
|
|
// IsASCII checks if the given byte array contains only ASCII characters
|
|
func IsASCII(data []byte) bool {
|
|
for _, b := range data {
|
|
// Check for non-control ASCII characters and common control characters
|
|
if (b < 32 && b != 9 && b != 10 && b != 13) || b > 126 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// FormatServiceEnvelope formats a ServiceEnvelope message into a human-readable string
|
|
func FormatServiceEnvelope(envelope *pb.ServiceEnvelope) string {
|
|
var builder strings.Builder
|
|
|
|
builder.WriteString("ServiceEnvelope:\n")
|
|
|
|
// Print basic envelope info
|
|
builder.WriteString(fmt.Sprintf(" Channel ID: %s\n", envelope.GetChannelId()))
|
|
builder.WriteString(fmt.Sprintf(" Gateway ID: %s\n", envelope.GetGatewayId()))
|
|
|
|
// Print MeshPacket info if available
|
|
if packet := envelope.GetPacket(); packet != nil {
|
|
builder.WriteString("\nMeshPacket:\n")
|
|
builder.WriteString(fmt.Sprintf(" ID: %d\n", packet.GetId()))
|
|
builder.WriteString(fmt.Sprintf(" From: %d\n", packet.GetFrom()))
|
|
builder.WriteString(fmt.Sprintf(" To: %d\n", packet.GetTo()))
|
|
|
|
// Output routing and hop information
|
|
builder.WriteString(fmt.Sprintf(" Hop Limit: %d\n", packet.GetHopLimit()))
|
|
builder.WriteString(fmt.Sprintf(" Hop Start: %d\n", packet.GetHopStart()))
|
|
builder.WriteString(fmt.Sprintf(" Want ACK: %v\n", packet.GetWantAck()))
|
|
builder.WriteString(fmt.Sprintf(" Priority: %s\n", packet.GetPriority()))
|
|
|
|
// Output if the packet was delivered via MQTT
|
|
if packet.GetViaMqtt() {
|
|
builder.WriteString(" Via MQTT: Yes\n")
|
|
}
|
|
|
|
// Relay and next hop info
|
|
if packet.GetNextHop() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Next Hop: %d\n", packet.GetNextHop()))
|
|
}
|
|
if packet.GetRelayNode() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Relay Node: %d\n", packet.GetRelayNode()))
|
|
}
|
|
|
|
// Show public key information if available (for PKI-encrypted packets)
|
|
if len(packet.GetPublicKey()) > 0 {
|
|
builder.WriteString(fmt.Sprintf(" Public Key: %x\n", packet.GetPublicKey()))
|
|
builder.WriteString(fmt.Sprintf(" PKI Encrypted: %v\n", packet.GetPkiEncrypted()))
|
|
}
|
|
|
|
// Determine payload type
|
|
if packet.GetDecoded() != nil {
|
|
data := packet.GetDecoded()
|
|
builder.WriteString(fmt.Sprintf("\n Decoded Data (Port: %s):\n", data.GetPortnum()))
|
|
|
|
// Output portnum-specific information
|
|
switch data.GetPortnum() {
|
|
case pb.PortNum_TEXT_MESSAGE_APP:
|
|
// Text message
|
|
builder.WriteString(fmt.Sprintf(" Text Message: %s\n", string(data.GetPayload())))
|
|
case pb.PortNum_TELEMETRY_APP:
|
|
// Try to decode telemetry data
|
|
builder.WriteString(" Telemetry Data\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
case pb.PortNum_NODEINFO_APP:
|
|
// Node information
|
|
builder.WriteString(" Node Information\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
case pb.PortNum_POSITION_APP:
|
|
// Position data
|
|
builder.WriteString(" Position Data\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
default:
|
|
// For other message types, print the payload as hex
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
}
|
|
|
|
// Show additional Data fields
|
|
if data.GetRequestId() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Request ID: %d\n", data.GetRequestId()))
|
|
}
|
|
if data.GetReplyId() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Reply ID: %d\n", data.GetReplyId()))
|
|
}
|
|
if data.GetEmoji() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Emoji: %d\n", data.GetEmoji()))
|
|
}
|
|
if data.GetDest() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Destination Node: %d\n", data.GetDest()))
|
|
}
|
|
if data.GetSource() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Source Node: %d\n", data.GetSource()))
|
|
}
|
|
if data.GetWantResponse() {
|
|
builder.WriteString(" Wants Response: Yes\n")
|
|
}
|
|
|
|
} else if packet.GetEncrypted() != nil {
|
|
// Encrypted payload information
|
|
builder.WriteString("\n Encrypted Payload:\n")
|
|
builder.WriteString(fmt.Sprintf(" Size: %d bytes\n", len(packet.GetEncrypted())))
|
|
builder.WriteString(fmt.Sprintf(" Channel: %d\n", packet.GetChannel()))
|
|
|
|
// Print the first few bytes of the encrypted payload for identification
|
|
if len(packet.GetEncrypted()) > 0 {
|
|
displayLen := len(packet.GetEncrypted())
|
|
if displayLen > 16 {
|
|
displayLen = 16
|
|
}
|
|
builder.WriteString(fmt.Sprintf(" First %d bytes: %x\n", displayLen, packet.GetEncrypted()[:displayLen]))
|
|
}
|
|
|
|
// If the packet has channel ID, it's using channel-based encryption
|
|
channelId := envelope.GetChannelId()
|
|
if channelId != "" {
|
|
builder.WriteString(fmt.Sprintf(" Encryption: Channel-based (Channel ID: %s)\n", channelId))
|
|
|
|
// Attempt to decrypt the payload using the channel key
|
|
channelKey := GetChannelKey(channelId)
|
|
builder.WriteString(fmt.Sprintf(" Using key (%d bytes): %x\n", len(channelKey), channelKey))
|
|
|
|
// Try to decrypt
|
|
decrypted, err := XOR(packet.GetEncrypted(), channelKey, packet.GetId(), packet.GetFrom())
|
|
if err != nil {
|
|
builder.WriteString(fmt.Sprintf(" Decryption error: %v\n", err))
|
|
} else {
|
|
builder.WriteString(fmt.Sprintf(" Decrypted (%d bytes): %x\n", len(decrypted), decrypted))
|
|
|
|
// Try to parse the decrypted payload as a Data message
|
|
var data pb.Data
|
|
if err := proto.Unmarshal(decrypted, &data); err == nil {
|
|
// Successfully decoded the decrypted payload
|
|
builder.WriteString(fmt.Sprintf("\n Decoded Data (Port: %s):\n", data.GetPortnum()))
|
|
|
|
// Output portnum-specific information
|
|
switch data.GetPortnum() {
|
|
case pb.PortNum_TEXT_MESSAGE_APP:
|
|
// Text message
|
|
builder.WriteString(fmt.Sprintf(" Text Message: %s\n", string(data.GetPayload())))
|
|
case pb.PortNum_TELEMETRY_APP:
|
|
// Telemetry data
|
|
builder.WriteString(" Telemetry Data\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
case pb.PortNum_NODEINFO_APP:
|
|
// Node information
|
|
builder.WriteString(" Node Information\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
case pb.PortNum_POSITION_APP:
|
|
// Position data
|
|
builder.WriteString(" Position Data\n")
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
default:
|
|
// For other message types, print the payload as hex
|
|
builder.WriteString(fmt.Sprintf(" Payload (%d bytes): %x\n", len(data.GetPayload()), data.GetPayload()))
|
|
}
|
|
|
|
// Show additional Data fields
|
|
if data.GetRequestId() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Request ID: %d\n", data.GetRequestId()))
|
|
}
|
|
if data.GetReplyId() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Reply ID: %d\n", data.GetReplyId()))
|
|
}
|
|
if data.GetEmoji() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Emoji: %d\n", data.GetEmoji()))
|
|
}
|
|
if data.GetDest() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Destination Node: %d\n", data.GetDest()))
|
|
}
|
|
if data.GetSource() != 0 {
|
|
builder.WriteString(fmt.Sprintf(" Source Node: %d\n", data.GetSource()))
|
|
}
|
|
if data.GetWantResponse() {
|
|
builder.WriteString(" Wants Response: Yes\n")
|
|
}
|
|
} else {
|
|
// If we couldn't parse as Data, try to interpret as text
|
|
if IsASCII(decrypted) {
|
|
builder.WriteString(fmt.Sprintf(" Decrypted as text: %s\n", string(decrypted)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use protojson to generate a full JSON representation for debugging
|
|
marshaler := protojson.MarshalOptions{
|
|
Multiline: true,
|
|
Indent: " ",
|
|
}
|
|
jsonBytes, err := marshaler.Marshal(envelope)
|
|
if err == nil {
|
|
builder.WriteString("\nFull Protobuf Structure:\n")
|
|
builder.WriteString(string(jsonBytes))
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// FormatJSONMessage formats a JSON message into a human-readable string
|
|
func FormatJSONMessage(jsonData map[string]interface{}) string {
|
|
var builder strings.Builder
|
|
|
|
builder.WriteString("JSON Message:\n")
|
|
|
|
// Extract and display common fields
|
|
if from, ok := jsonData["from"].(string); ok {
|
|
builder.WriteString(fmt.Sprintf(" From: %s\n", from))
|
|
}
|
|
if to, ok := jsonData["to"].(string); ok {
|
|
builder.WriteString(fmt.Sprintf(" To: %s\n", to))
|
|
}
|
|
if message, ok := jsonData["payload"].(string); ok {
|
|
builder.WriteString(fmt.Sprintf(" Message: %s\n", message))
|
|
}
|
|
if timestamp, ok := jsonData["timestamp"].(string); ok {
|
|
builder.WriteString(fmt.Sprintf(" Timestamp: %s\n", timestamp))
|
|
}
|
|
|
|
// Format the full JSON for reference
|
|
jsonBytes, err := json.MarshalIndent(jsonData, " ", " ")
|
|
if err == nil {
|
|
builder.WriteString("\nFull JSON Structure:\n ")
|
|
builder.WriteString(string(jsonBytes))
|
|
}
|
|
|
|
return builder.String()
|
|
}
|
|
|
|
// FormatMessage formats a decoded message based on its format
|
|
func FormatMessage(topicInfo *TopicInfo, payload []byte) string {
|
|
var builder strings.Builder
|
|
|
|
// Display topic information
|
|
builder.WriteString(fmt.Sprintf("Topic: %s\n", topicInfo.FullTopic))
|
|
builder.WriteString(fmt.Sprintf("Region Path: %s\n", topicInfo.RegionPath))
|
|
builder.WriteString(fmt.Sprintf("Version: %s\n", topicInfo.Version))
|
|
builder.WriteString(fmt.Sprintf("Format: %s\n", topicInfo.Format))
|
|
builder.WriteString(fmt.Sprintf("Channel: %s\n", topicInfo.Channel))
|
|
if topicInfo.UserID != "" {
|
|
builder.WriteString(fmt.Sprintf("User ID: %s\n", topicInfo.UserID))
|
|
}
|
|
builder.WriteString("\n")
|
|
|
|
// Decode and format based on the format
|
|
if topicInfo.Format == "e" {
|
|
// Encoded protobuf message (ServiceEnvelope)
|
|
serviceEnvelope, err := DecodeEncodedMessage(payload)
|
|
if err != nil {
|
|
builder.WriteString(fmt.Sprintf("Error decoding encoded message: %v\n", err))
|
|
builder.WriteString(fmt.Sprintf("Raw Binary (%d bytes): %x\n", len(payload), payload))
|
|
} else {
|
|
builder.WriteString(FormatServiceEnvelope(serviceEnvelope))
|
|
}
|
|
} else if topicInfo.Format == "json" {
|
|
// JSON message
|
|
jsonData, err := DecodeJSONMessage(payload)
|
|
if err != nil {
|
|
builder.WriteString(fmt.Sprintf("Error decoding JSON message: %v\n", err))
|
|
builder.WriteString(fmt.Sprintf("Raw Data: %s\n", string(payload)))
|
|
} else {
|
|
builder.WriteString(FormatJSONMessage(jsonData))
|
|
}
|
|
} else {
|
|
// Unknown format
|
|
builder.WriteString(fmt.Sprintf("Unsupported format: %s\n", topicInfo.Format))
|
|
builder.WriteString(fmt.Sprintf("Raw Data (%d bytes): %x\n", len(payload), payload))
|
|
}
|
|
|
|
return builder.String()
|
|
} |