更新机器人接收功能
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pkiKeyResolver 是 mqtpp 在解密 PKI 加密包时回调的接收者私钥/发送者公钥查询函数。
|
||||||
|
//
|
||||||
|
// to 是接收者节点号(应该匹配某个本地受管的 bot),from 是发送者节点号(应该已经有 nodeinfo 上报)。
|
||||||
|
// 返回的 ok=false 时调用方会跳过 PKI 路径并回落到 channel PSK 解密。
|
||||||
|
func newPKIKeyResolver(s *store) func(toNodeNum, fromNodeNum uint32) ([]byte, []byte, bool) {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return func(toNodeNum, fromNodeNum uint32) ([]byte, []byte, bool) {
|
||||||
|
bot, err := s.GetBotNodeByNodeNum(int64(toNodeNum))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
privateKeyB64 := strings.TrimSpace(bot.PrivateKey)
|
||||||
|
if privateKeyB64 == "" {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
privateKey, err := base64.StdEncoding.DecodeString(privateKeyB64)
|
||||||
|
if err != nil || len(privateKey) != 32 {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
fromPublic, ok := lookupNodeInfoPublicKey(s, fromNodeNum)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return privateKey, fromPublic, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupNodeInfoPublicKey 在 nodeinfo 表中按 node_num 查 X25519 公钥,
|
||||||
|
// 兼容 hex 与 base64 两种历史存储格式。
|
||||||
|
func lookupNodeInfoPublicKey(s *store, nodeNum uint32) ([]byte, bool) {
|
||||||
|
var row nodeInfoRecord
|
||||||
|
if err := s.db.Where("node_num = ?", int64(nodeNum)).Take(&row).Error; err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if row.PublicKey == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
value := strings.TrimSpace(*row.PublicKey)
|
||||||
|
if value == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if decoded, err := hex.DecodeString(value); err == nil && len(decoded) == 32 {
|
||||||
|
return decoded, true
|
||||||
|
}
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(value); err == nil && len(decoded) == 32 {
|
||||||
|
return decoded, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBotNodeByNodeNum 按节点号查找受管 bot 节点;用于 PKI 解密时把 to 字段映射回本地私钥。
|
||||||
|
func (s *store) GetBotNodeByNodeNum(nodeNum int64) (*botNodeRecord, error) {
|
||||||
|
if s == nil || s.db == nil {
|
||||||
|
return nil, errors.New("store not configured")
|
||||||
|
}
|
||||||
|
var row botNodeRecord
|
||||||
|
if err := s.db.Where("node_num = ?", nodeNum).Take(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
@@ -35,11 +35,12 @@ const (
|
|||||||
|
|
||||||
type meshtasticFilterHook struct {
|
type meshtasticFilterHook struct {
|
||||||
mqtt.HookBase
|
mqtt.HookBase
|
||||||
key []byte
|
key []byte
|
||||||
dbQueue *dbWriteQueue
|
dbQueue *dbWriteQueue
|
||||||
stats *meshtasticMessageStats
|
stats *meshtasticMessageStats
|
||||||
blocking *blockingCache
|
blocking *blockingCache
|
||||||
settings *runtimeSettingsCache
|
settings *runtimeSettingsCache
|
||||||
|
pkiResolver func(toNodeNum, fromNodeNum uint32) ([]byte, []byte, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID 返回用于识别 Meshtastic payload 过滤器的 hook 名称。
|
// ID 返回用于识别 Meshtastic payload 过滤器的 hook 名称。
|
||||||
@@ -64,7 +65,10 @@ func (h *meshtasticFilterHook) OnConnect(cl *mqtt.Client, pk packets.Packet) err
|
|||||||
|
|
||||||
// OnPublish 在 broker 转发消息前校验 payload;无效消息会被拒绝并丢弃。
|
// OnPublish 在 broker 转发消息前校验 payload;无效消息会被拒绝并丢弃。
|
||||||
func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) {
|
func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) {
|
||||||
valid, _, record := mqtpp.MQTTPP(pk.TopicName, pk.Payload, h.key, mqtpp.Options{AllowEncryptedForwarding: h.settings.AllowEncryptedForwarding()})
|
valid, _, record := mqtpp.MQTTPP(pk.TopicName, pk.Payload, h.key, mqtpp.Options{
|
||||||
|
AllowEncryptedForwarding: h.settings.AllowEncryptedForwarding(),
|
||||||
|
PKIKeyResolver: h.pkiResolver,
|
||||||
|
})
|
||||||
if !valid {
|
if !valid {
|
||||||
h.rejectPublish(cl, pk, record)
|
h.rejectPublish(cl, pk, record)
|
||||||
return pk, packets.ErrRejectPacket
|
return pk, packets.ErrRejectPacket
|
||||||
@@ -221,7 +225,7 @@ func run(cfg *config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messageStats := &meshtasticMessageStats{}
|
messageStats := &meshtasticMessageStats{}
|
||||||
server, mqttAddr, err := startMQTTServer(cfg, dbQueue, messageStats, blocking, settings)
|
server, mqttAddr, err := startMQTTServer(cfg, store, dbQueue, messageStats, blocking, settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -285,12 +289,20 @@ func run(cfg *config) error {
|
|||||||
return runErr
|
return runErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMQTTServer(cfg *config, dbQueue *dbWriteQueue, stats *meshtasticMessageStats, blocking *blockingCache, settings *runtimeSettingsCache) (*mqtt.Server, string, error) {
|
func startMQTTServer(cfg *config, store *store, dbQueue *dbWriteQueue, stats *meshtasticMessageStats, blocking *blockingCache, settings *runtimeSettingsCache) (*mqtt.Server, string, error) {
|
||||||
server := mqtt.New(&mqtt.Options{InlineClient: true})
|
server := mqtt.New(&mqtt.Options{InlineClient: true})
|
||||||
if err := server.AddHook(new(auth.AllowHook), nil); err != nil {
|
if err := server.AddHook(new(auth.AllowHook), nil); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
if err := server.AddHook(&meshtasticFilterHook{key: cfg.key, dbQueue: dbQueue, stats: stats, blocking: blocking, settings: settings}, nil); err != nil {
|
hook := &meshtasticFilterHook{
|
||||||
|
key: cfg.key,
|
||||||
|
dbQueue: dbQueue,
|
||||||
|
stats: stats,
|
||||||
|
blocking: blocking,
|
||||||
|
settings: settings,
|
||||||
|
pkiResolver: newPKIKeyResolver(store),
|
||||||
|
}
|
||||||
|
if err := server.AddHook(hook, nil); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-6
@@ -34,6 +34,10 @@ var defaultMeshtasticPSK = []byte{
|
|||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
AllowEncryptedForwarding bool
|
AllowEncryptedForwarding bool
|
||||||
|
// PKIKeyResolver 在解密 PKI 加密包时被调用:toNodeNum 是包的接收者节点号(应为本地受管节点,
|
||||||
|
// 例如机器人),fromNodeNum 是发送方节点号。回调需要返回接收方的 X25519 私钥(32B)和发送方
|
||||||
|
// 的 X25519 公钥(32B)。当回调缺失或返回 ok=false 时,PKI 解密会被跳过(仍尝试 channel PSK)。
|
||||||
|
PKIKeyResolver func(toNodeNum, fromNodeNum uint32) (privateKey, fromPublicKey []byte, ok bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceEnvelope struct {
|
type serviceEnvelope struct {
|
||||||
@@ -126,7 +130,7 @@ func MQTTPP(topic string, raw []byte, key []byte, opts Options) (bool, []byte, m
|
|||||||
//解包失败
|
//解包失败
|
||||||
return false, nil, map[string]any{"topic": topic, "error": "protobuf decode failed: " + err.Error(), "payload_len": len(raw)}
|
return false, nil, map[string]any{"topic": topic, "error": "protobuf decode failed: " + err.Error(), "payload_len": len(raw)}
|
||||||
}
|
}
|
||||||
record, err := describePacket(topic, env, key)
|
record, err := describePacket(topic, env, key, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//解码失败
|
//解码失败
|
||||||
return false, nil, map[string]any{"topic": topic, "error": err.Error(), "payload_len": len(raw)}
|
return false, nil, map[string]any{"topic": topic, "error": err.Error(), "payload_len": len(raw)}
|
||||||
@@ -664,7 +668,7 @@ func varintValue(typ protowire.Type, value any) uint64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// describePacket 根据 ServiceEnvelope 和 PSK 生成统一的 JSON 记录字段。
|
// describePacket 根据 ServiceEnvelope 和 PSK 生成统一的 JSON 记录字段。
|
||||||
func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]any, error) {
|
func describePacket(topic string, env *serviceEnvelope, key []byte, opts Options) (map[string]any, error) {
|
||||||
packet := env.Packet
|
packet := env.Packet
|
||||||
if packet == nil {
|
if packet == nil {
|
||||||
packet = &meshPacket{}
|
packet = &meshPacket{}
|
||||||
@@ -685,7 +689,7 @@ func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
if packet.PayloadVariant == "encrypted" {
|
if packet.PayloadVariant == "encrypted" {
|
||||||
decryptedPacket, decryptStatus := tryDecryptPacket(packet, env.ChannelID, key)
|
decryptedPacket, decryptStatus := tryDecryptPacket(packet, env.ChannelID, key, opts)
|
||||||
if decryptedPacket == nil {
|
if decryptedPacket == nil {
|
||||||
return merge(base, map[string]any{
|
return merge(base, map[string]any{
|
||||||
"type": "encrypted_packet",
|
"type": "encrypted_packet",
|
||||||
@@ -697,13 +701,15 @@ func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]
|
|||||||
|
|
||||||
decryptedEnv := *env
|
decryptedEnv := *env
|
||||||
decryptedEnv.Packet = decryptedPacket
|
decryptedEnv.Packet = decryptedPacket
|
||||||
decrypted, err := describePacket(topic, &decryptedEnv, key)
|
decrypted, err := describePacket(topic, &decryptedEnv, key, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
decrypted["payload_variant"] = "decoded"
|
decrypted["payload_variant"] = "decoded"
|
||||||
decrypted["decrypt_success"] = true
|
decrypted["decrypt_success"] = true
|
||||||
decrypted["decrypt_status"] = decryptStatus
|
decrypted["decrypt_status"] = decryptStatus
|
||||||
|
// PKI 解密的包要保留 pki_encrypted 标记(tryDecryptPacket 在成功后会把它标记到 packet 上)
|
||||||
|
decrypted["pki_encrypted"] = decryptedPacket.PKIEncrypted
|
||||||
return decrypted, nil
|
return decrypted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,8 +758,22 @@ func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryDecryptPacket 尝试用 channel PSK 解密 encrypted MeshPacket,并返回解密状态。
|
// tryDecryptPacket 尝试解密 encrypted MeshPacket,并返回解密状态。
|
||||||
func tryDecryptPacket(packet *meshPacket, channelID string, key []byte) (*meshPacket, string) {
|
// 解密优先级(与固件 perhapsDecode 对齐):
|
||||||
|
// 1. 若包是 PKI 风格(channel=0、to 非广播、PSK 无关)且调用方提供了 PKIKeyResolver,
|
||||||
|
// 则用 X25519 + AES-CCM(M=8,L=2) 解密。
|
||||||
|
// 2. 否则回落到 channel PSK + AES-CTR 路径。
|
||||||
|
func tryDecryptPacket(packet *meshPacket, channelID string, key []byte, opts Options) (*meshPacket, string) {
|
||||||
|
// 先尝试 PKI 路径:固件发出的 PKI 包 channel=0、to 非广播、长度 > pkcOverhead。
|
||||||
|
// channel_id 字面量在 ServiceEnvelope 上一般是 "PKI",但有些转发路径会保留原 channel 名,
|
||||||
|
// 因此这里以 channel 字段=0 + 注册了 resolver 为充分条件即尝试解密。
|
||||||
|
if opts.PKIKeyResolver != nil && packet.Channel == 0 && packet.To != 0 && packet.To != NodeNumBroadcast &&
|
||||||
|
len(packet.Encrypted) > pkcOverhead {
|
||||||
|
if decrypted, status, ok := tryDecryptPKIPacket(packet, opts.PKIKeyResolver); ok {
|
||||||
|
return decrypted, status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
return nil, "psk disables encryption"
|
return nil, "psk disables encryption"
|
||||||
}
|
}
|
||||||
@@ -780,6 +800,43 @@ func tryDecryptPacket(packet *meshPacket, channelID string, key []byte) (*meshPa
|
|||||||
return &decrypted, "success"
|
return &decrypted, "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryDecryptPKIPacket 用接收方私钥 + 发送方公钥派生共享密钥并 AES-CCM 解密。
|
||||||
|
// 第三个返回值表示是否“尝试且解出了合法 Data 包”——返回 false 时调用方会回落到 PSK 路径。
|
||||||
|
func tryDecryptPKIPacket(packet *meshPacket, resolver func(toNodeNum, fromNodeNum uint32) ([]byte, []byte, bool)) (*meshPacket, string, bool) {
|
||||||
|
privateKey, fromPublic, ok := resolver(packet.To, packet.From)
|
||||||
|
if !ok {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
if len(privateKey) != 32 || len(fromPublic) != 32 {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
encryptedLen := len(packet.Encrypted) - pkcOverhead
|
||||||
|
ciphertext := packet.Encrypted[:encryptedLen]
|
||||||
|
auth := packet.Encrypted[encryptedLen : encryptedLen+8]
|
||||||
|
extraNonce := binary.LittleEndian.Uint32(packet.Encrypted[encryptedLen+8:])
|
||||||
|
sharedKey, err := pkiSharedKey(privateKey, fromPublic)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
plaintext, err := aesCCMDecrypt(sharedKey, pkiNonce(packet.ID, packet.From, extraNonce), ciphertext, auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
decoded, err := parseDataPacket(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
if decoded.Portnum == unknownApp {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
decrypted := *packet
|
||||||
|
decrypted.Encrypted = nil
|
||||||
|
decrypted.Decoded = decoded
|
||||||
|
decrypted.PayloadVariant = "decoded"
|
||||||
|
decrypted.PKIEncrypted = true
|
||||||
|
return &decrypted, "pki success", true
|
||||||
|
}
|
||||||
|
|
||||||
// decodeUser 将 NODEINFO_APP payload 解码为节点信息 JSON 字段。
|
// decodeUser 将 NODEINFO_APP payload 解码为节点信息 JSON 字段。
|
||||||
func decodeUser(packet *meshPacket) (map[string]any, error) {
|
func decodeUser(packet *meshPacket) (map[string]any, error) {
|
||||||
user, err := parseUser(packet.Decoded.Payload)
|
user, err := parseUser(packet.Decoded.Payload)
|
||||||
|
|||||||
@@ -159,3 +159,51 @@ func TestBuildPKIMeshPacketTags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 端到端:发送方构造 PKI 包,接收方通过 PKIKeyResolver 解密并还原文本消息记录。
|
||||||
|
func TestMQTTPPDecryptsPKIWithResolver(t *testing.T) {
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
senderPriv, _ := curve.GenerateKey(rand.Reader)
|
||||||
|
recipientPriv, _ := curve.GenerateKey(rand.Reader)
|
||||||
|
|
||||||
|
const text = "hello PKI inbound"
|
||||||
|
const fromNum uint32 = 0xaaaa1111
|
||||||
|
const toNum uint32 = 0xbbbb2222
|
||||||
|
const packetID uint32 = 0x77777777
|
||||||
|
|
||||||
|
raw, err := BuildPKITextMessageServiceEnvelope(PKITextMessageBuildOptions{
|
||||||
|
FromNodeNum: fromNum,
|
||||||
|
ToNodeNum: toNum,
|
||||||
|
PacketID: packetID,
|
||||||
|
GatewayID: NodeNumToID(fromNum),
|
||||||
|
ViaMQTT: true,
|
||||||
|
SenderPrivate: senderPriv.Bytes(),
|
||||||
|
RecipientPub: recipientPriv.PublicKey().Bytes(),
|
||||||
|
SenderPublic: senderPriv.PublicKey().Bytes(),
|
||||||
|
Text: text,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("build: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := func(to, from uint32) ([]byte, []byte, bool) {
|
||||||
|
if to != toNum || from != fromNum {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
return recipientPriv.Bytes(), senderPriv.PublicKey().Bytes(), true
|
||||||
|
}
|
||||||
|
dummyPSK, _ := ExpandPSK("AQ==")
|
||||||
|
valid, _, record := MQTTPP("msh/2/e/PKI/!aaaa1111", raw, dummyPSK, Options{PKIKeyResolver: resolver})
|
||||||
|
if !valid {
|
||||||
|
t.Fatalf("MQTTPP not valid: %#v", record)
|
||||||
|
}
|
||||||
|
if record["type"] != "text_message" {
|
||||||
|
t.Fatalf("type = %v, want text_message", record["type"])
|
||||||
|
}
|
||||||
|
if record["text"] != text {
|
||||||
|
t.Fatalf("text = %v", record["text"])
|
||||||
|
}
|
||||||
|
if record["pki_encrypted"] != true {
|
||||||
|
t.Fatalf("pki_encrypted = %v", record["pki_encrypted"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user