mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
Compare commits
1 Commits
feat/provi
...
l5y-web-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb7a09cb6f |
@@ -29,7 +29,9 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
modem_preset TEXT,
|
||||
channel_name TEXT,
|
||||
reply_id INTEGER,
|
||||
emoji TEXT
|
||||
emoji TEXT,
|
||||
decrypted INTEGER NOT NULL DEFAULT 0,
|
||||
decryption_confidence REAL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_rx_time ON messages(rx_time);
|
||||
|
||||
@@ -1497,6 +1497,7 @@ module PotatoMesh
|
||||
portnum: data[:portnum],
|
||||
payload: data[:payload],
|
||||
channel_name: channel_name,
|
||||
decryption_confidence: data[:decryption_confidence],
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1568,9 +1569,11 @@ module PotatoMesh
|
||||
decrypted_payload = nil
|
||||
decrypted_text = nil
|
||||
decrypted_portnum = nil
|
||||
decrypted_flag = false
|
||||
decryption_confidence = nil
|
||||
|
||||
if encrypted && (text.nil? || text.to_s.strip.empty?)
|
||||
decrypted = decrypt_meshtastic_message(
|
||||
decrypted_data = decrypt_meshtastic_message(
|
||||
message,
|
||||
msg_id,
|
||||
from_id,
|
||||
@@ -1578,17 +1581,19 @@ module PotatoMesh
|
||||
channel_index,
|
||||
)
|
||||
|
||||
if decrypted
|
||||
decrypted_payload = decrypted
|
||||
decrypted_portnum = decrypted[:portnum]
|
||||
if decrypted_data
|
||||
decrypted_payload = decrypted_data
|
||||
decrypted_portnum = decrypted_data[:portnum]
|
||||
|
||||
if decrypted[:text]
|
||||
text = decrypted[:text]
|
||||
if decrypted_data[:text]
|
||||
text = decrypted_data[:text]
|
||||
decrypted_text = text
|
||||
clear_encrypted = true
|
||||
encrypted = nil
|
||||
message["text"] = text
|
||||
message["channel_name"] ||= decrypted[:channel_name]
|
||||
message["channel_name"] ||= decrypted_data[:channel_name]
|
||||
decrypted_flag = true
|
||||
decryption_confidence = decrypted_data[:decryption_confidence] || 0.0
|
||||
if portnum.nil? && decrypted_portnum
|
||||
portnum = decrypted_portnum
|
||||
message["portnum"] = portnum
|
||||
@@ -1626,11 +1631,13 @@ module PotatoMesh
|
||||
channel_name,
|
||||
reply_id,
|
||||
emoji,
|
||||
decrypted_flag ? 1 : 0,
|
||||
decryption_confidence,
|
||||
]
|
||||
|
||||
with_busy_retry do
|
||||
existing = db.get_first_row(
|
||||
"SELECT from_id, to_id, text, encrypted, lora_freq, modem_preset, channel_name, reply_id, emoji, portnum FROM messages WHERE id = ?",
|
||||
"SELECT from_id, to_id, text, encrypted, lora_freq, modem_preset, channel_name, reply_id, emoji, portnum, decrypted, decryption_confidence FROM messages WHERE id = ?",
|
||||
[msg_id],
|
||||
)
|
||||
if existing
|
||||
@@ -1685,6 +1692,11 @@ module PotatoMesh
|
||||
updates["rx_iso"] = rx_iso if rx_iso
|
||||
end
|
||||
|
||||
if clear_encrypted
|
||||
updates["decrypted"] = 1
|
||||
updates["decryption_confidence"] = decryption_confidence
|
||||
end
|
||||
|
||||
if portnum
|
||||
existing_portnum = existing.is_a?(Hash) ? existing["portnum"] : existing[9]
|
||||
existing_portnum_str = existing_portnum&.to_s
|
||||
@@ -1737,8 +1749,8 @@ module PotatoMesh
|
||||
|
||||
begin
|
||||
db.execute <<~SQL, row
|
||||
INSERT INTO messages(id,rx_time,rx_iso,from_id,to_id,channel,portnum,text,encrypted,snr,rssi,hop_limit,lora_freq,modem_preset,channel_name,reply_id,emoji)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
INSERT INTO messages(id,rx_time,rx_iso,from_id,to_id,channel,portnum,text,encrypted,snr,rssi,hop_limit,lora_freq,modem_preset,channel_name,reply_id,emoji,decrypted,decryption_confidence)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
SQL
|
||||
rescue SQLite3::ConstraintException
|
||||
existing_row = db.get_first_row(
|
||||
@@ -1759,6 +1771,10 @@ module PotatoMesh
|
||||
fallback_updates["encrypted"] = encrypted if encrypted && allow_encrypted_update
|
||||
fallback_updates["encrypted"] = nil if clear_encrypted
|
||||
fallback_updates["portnum"] = portnum if portnum
|
||||
if clear_encrypted
|
||||
fallback_updates["decrypted"] = 1
|
||||
fallback_updates["decryption_confidence"] = decryption_confidence
|
||||
end
|
||||
if decrypted_precedence
|
||||
fallback_updates["channel"] = message["channel"] if message.key?("channel")
|
||||
fallback_updates["snr"] = message["snr"] if message.key?("snr")
|
||||
|
||||
@@ -150,6 +150,16 @@ module PotatoMesh
|
||||
message_columns << "emoji"
|
||||
end
|
||||
|
||||
unless message_columns.include?("decrypted")
|
||||
db.execute("ALTER TABLE messages ADD COLUMN decrypted INTEGER NOT NULL DEFAULT 0")
|
||||
message_columns << "decrypted"
|
||||
end
|
||||
|
||||
unless message_columns.include?("decryption_confidence")
|
||||
db.execute("ALTER TABLE messages ADD COLUMN decryption_confidence REAL")
|
||||
message_columns << "decryption_confidence"
|
||||
end
|
||||
|
||||
reply_index_exists =
|
||||
db.get_first_value(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND name='idx_messages_reply_id'",
|
||||
|
||||
@@ -29,6 +29,8 @@ module PotatoMesh
|
||||
|
||||
DEFAULT_PSK_B64 = "AQ=="
|
||||
TEXT_MESSAGE_PORTNUM = 1
|
||||
# Number of characters required for full confidence scoring.
|
||||
CONFIDENCE_LENGTH_TARGET = 8.0
|
||||
|
||||
# Decrypt an encrypted Meshtastic payload into UTF-8 text.
|
||||
#
|
||||
@@ -78,12 +80,21 @@ module PotatoMesh
|
||||
return nil unless data
|
||||
|
||||
text = nil
|
||||
decryption_confidence = nil
|
||||
if data[:portnum] == TEXT_MESSAGE_PORTNUM
|
||||
candidate = data[:payload].dup.force_encoding("UTF-8")
|
||||
text = candidate if candidate.valid_encoding? && !candidate.empty?
|
||||
if candidate.valid_encoding? && !candidate.empty?
|
||||
text = candidate
|
||||
decryption_confidence = text_confidence(text)
|
||||
end
|
||||
end
|
||||
|
||||
{ portnum: data[:portnum], payload: data[:payload], text: text }
|
||||
{
|
||||
portnum: data[:portnum],
|
||||
payload: data[:payload],
|
||||
text: text,
|
||||
decryption_confidence: decryption_confidence,
|
||||
}
|
||||
rescue ArgumentError, OpenSSL::Cipher::CipherError
|
||||
nil
|
||||
end
|
||||
@@ -154,6 +165,25 @@ module PotatoMesh
|
||||
nil
|
||||
end
|
||||
|
||||
# Score the plausibility of decrypted text content.
|
||||
#
|
||||
# @param text [String] decrypted text candidate.
|
||||
# @return [Float] confidence score between 0.0 and 1.0.
|
||||
def text_confidence(text)
|
||||
return 0.0 unless text.is_a?(String)
|
||||
return 0.0 if text.empty?
|
||||
|
||||
total = text.length.to_f
|
||||
length_score = [total / CONFIDENCE_LENGTH_TARGET, 1.0].min
|
||||
control_count = text.scan(/[\p{Cc}\p{Cs}]/).length
|
||||
control_ratio = control_count / total
|
||||
acceptable_count = text.scan(/[\p{L}\p{N}\p{P}\p{S}\p{Zs}\t\n\r]/).length
|
||||
acceptable_ratio = acceptable_count / total
|
||||
|
||||
score = length_score * acceptable_ratio * (1.0 - control_ratio)
|
||||
score.clamp(0.0, 1.0)
|
||||
end
|
||||
|
||||
# Resolve the node number from any of the supported identifiers.
|
||||
#
|
||||
# @param from_id [String, nil] Meshtastic node identifier.
|
||||
|
||||
@@ -357,7 +357,7 @@ module PotatoMesh
|
||||
SELECT m.id, m.rx_time, m.rx_iso, m.from_id, m.to_id, m.channel,
|
||||
m.portnum, m.text, m.encrypted, m.rssi, m.hop_limit,
|
||||
m.lora_freq, m.modem_preset, m.channel_name, m.snr,
|
||||
m.reply_id, m.emoji
|
||||
m.reply_id, m.emoji, m.decrypted, m.decryption_confidence
|
||||
FROM messages m
|
||||
SQL
|
||||
sql += " WHERE #{where_clauses.join(" AND ")}\n"
|
||||
@@ -374,6 +374,27 @@ module PotatoMesh
|
||||
if string_or_nil(r["encrypted"])
|
||||
r.delete("portnum")
|
||||
end
|
||||
|
||||
if r.key?("decrypted")
|
||||
decrypted_raw = r["decrypted"]
|
||||
decrypted = case decrypted_raw
|
||||
when true, false
|
||||
decrypted_raw
|
||||
when Integer
|
||||
!decrypted_raw.zero?
|
||||
when String
|
||||
trimmed = decrypted_raw.strip
|
||||
!trimmed.empty? && trimmed != "0" && trimmed.casecmp("false") != 0
|
||||
else
|
||||
!!decrypted_raw
|
||||
end
|
||||
r["decrypted"] = decrypted
|
||||
r.delete("decryption_confidence") unless decrypted
|
||||
end
|
||||
|
||||
if r.key?("decryption_confidence") && !r["decryption_confidence"].nil?
|
||||
r["decryption_confidence"] = r["decryption_confidence"].to_f
|
||||
end
|
||||
if PotatoMesh::Config.debug? && (r["from_id"].nil? || r["from_id"].to_s.strip.empty?)
|
||||
raw = db.execute("SELECT * FROM messages WHERE id = ?", [r["id"]]).first
|
||||
debug_log(
|
||||
|
||||
@@ -4007,13 +4007,16 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
with_db(readonly: true) do |db|
|
||||
db.results_as_hash = true
|
||||
row = db.get_first_row(
|
||||
"SELECT text, encrypted, channel_name FROM messages WHERE id = ?",
|
||||
"SELECT text, encrypted, channel_name, decrypted, decryption_confidence FROM messages WHERE id = ?",
|
||||
[payload["packet_id"]],
|
||||
)
|
||||
|
||||
expect(row["text"]).to eq("Nabend")
|
||||
expect(row["encrypted"]).to be_nil
|
||||
expect(row["channel_name"]).to eq("BerlinMesh")
|
||||
expect(row["decrypted"]).to eq(1)
|
||||
expect(row["decryption_confidence"]).to be > 0.0
|
||||
expect(row["decryption_confidence"]).to be <= 1.0
|
||||
end
|
||||
ensure
|
||||
if previous_psk.nil?
|
||||
@@ -4087,13 +4090,15 @@ RSpec.describe "Potato Mesh Sinatra app" do
|
||||
with_db(readonly: true) do |db|
|
||||
db.results_as_hash = true
|
||||
row = db.get_first_row(
|
||||
"SELECT text, encrypted, portnum FROM messages WHERE id = ?",
|
||||
"SELECT text, encrypted, portnum, decrypted, decryption_confidence FROM messages WHERE id = ?",
|
||||
[payload["packet_id"]],
|
||||
)
|
||||
|
||||
expect(row["text"]).to be_nil
|
||||
expect(row["encrypted"]).to eq(encrypted_payload)
|
||||
expect(row["portnum"]).to be_nil
|
||||
expect(row["decrypted"]).to eq(0)
|
||||
expect(row["decryption_confidence"]).to be_nil
|
||||
end
|
||||
ensure
|
||||
if previous_psk.nil?
|
||||
|
||||
@@ -166,6 +166,20 @@ RSpec.describe PotatoMesh::App::Database do
|
||||
expect(telemetry_columns).to include("rx_time", "battery_level")
|
||||
end
|
||||
|
||||
it "adds decryption metadata columns to existing messages tables" do
|
||||
SQLite3::Database.new(PotatoMesh::Config.db_path) do |db|
|
||||
db.execute("CREATE TABLE nodes(node_id TEXT)")
|
||||
db.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY)")
|
||||
end
|
||||
|
||||
expect(column_names_for("messages")).not_to include("decrypted", "decryption_confidence")
|
||||
|
||||
harness_class.ensure_schema_upgrades
|
||||
|
||||
message_columns = column_names_for("messages")
|
||||
expect(message_columns).to include("decrypted", "decryption_confidence")
|
||||
end
|
||||
|
||||
it "creates trace tables when absent" do
|
||||
SQLite3::Database.new(PotatoMesh::Config.db_path) do |db|
|
||||
db.execute("CREATE TABLE nodes(node_id TEXT)")
|
||||
|
||||
@@ -143,6 +143,18 @@ RSpec.describe PotatoMesh::App::Meshtastic::Cipher do
|
||||
expect(text).to eq("Nabend")
|
||||
end
|
||||
|
||||
it "captures a confidence score for decrypted text" do
|
||||
data = described_class.decrypt_data(
|
||||
cipher_b64: cipher_b64,
|
||||
packet_id: packet_id,
|
||||
from_id: from_id,
|
||||
psk_b64: psk_b64,
|
||||
)
|
||||
|
||||
expect(data[:text]).to eq("Nabend")
|
||||
expect(data[:decryption_confidence]).to be_between(0.0, 1.0)
|
||||
end
|
||||
|
||||
it "decrypts the public PSK alias sample payload" do
|
||||
text = described_class.decrypt_text(
|
||||
cipher_b64: "otu3OyMrTIUlcaisLVDyAnLW",
|
||||
@@ -196,7 +208,7 @@ RSpec.describe PotatoMesh::App::Meshtastic::Cipher do
|
||||
)
|
||||
|
||||
expect(text).to be_nil
|
||||
expect(data).to eq({ portnum: 3, payload: payload, text: nil })
|
||||
expect(data).to eq({ portnum: 3, payload: payload, text: nil, decryption_confidence: nil })
|
||||
end
|
||||
|
||||
it "normalizes packet ids from numeric strings" do
|
||||
@@ -277,4 +289,12 @@ RSpec.describe PotatoMesh::App::Meshtastic::Cipher do
|
||||
|
||||
expect(data).to be_nil
|
||||
end
|
||||
|
||||
it "scores text confidence higher for longer printable content" do
|
||||
low = described_class.text_confidence("AC")
|
||||
high = described_class.text_confidence("This looks like a sentence.")
|
||||
|
||||
expect(low).to be < high
|
||||
expect(high).to be_between(0.0, 1.0)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user