pwn 0.5.736 → 0.5.737
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/documentation/pwn-REPL.md +27 -0
- data/lib/pwn/plugins/repl/mesh.rb +181 -36
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +13 -0
- data/spec/lib/pwn/plugins/repl_mesh_dm_spec.rb +17 -0
- data/spec/lib/pwn/plugins/repl_mesh_dm_transport_spec.rb +50 -0
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +47 -0
- data/spec/lib/pwn/plugins/repl_mesh_key_exchange_spec.rb +85 -0
- data/spec/lib/pwn/plugins/repl_mesh_node_id_spec.rb +36 -0
- data/spec/lib/pwn/plugins/repl_mesh_rx_paint_spec.rb +23 -0
- data/spec/lib/pwn/plugins/repl_mesh_security_paint_spec.rb +36 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +3 -3
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af2001b2546112668f86a37ec9ab15c0bea62dd138188fd47b0441e2101a8622
|
|
4
|
+
data.tar.gz: 9281f66b1e6563d1abe0cd057b3a8a7e6de5c9712dd58e72e188f24d51acfbfc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e8f434c799f8cb9c67fb9d8f2f329e1e172fa1664ccfaea7bcb208febe9e3d8b42372b95210c3afb551cdb5648816f033fccdacb6897e36177a17d3b3be79a5a
|
|
7
|
+
data.tar.gz: 893e79ee5521e6c52558ea5cd69ac30c65b5a97e4a0cad3405da69bef72d48424714f971755880b8d7ca89168befe138d8edcc03636e124a09279ec7eb09c272
|
data/documentation/pwn-REPL.md
CHANGED
|
@@ -54,6 +54,33 @@ Typed at the mesh TX prompt (leading `/`). `/` or `/menu` opens a boxed curses m
|
|
|
54
54
|
|
|
55
55
|
Typing `/` shows matching commands in the TX pane (ncurses hides Reline's dropdown). TAB still completes `/…` commands, channel names, transports, and device ids.
|
|
56
56
|
|
|
57
|
+
In COMPOSE, Up recalls older submitted lines and Down recalls newer ones.
|
|
58
|
+
Moving past the newest entry restores the draft and cursor position from before
|
|
59
|
+
history browsing. Recalled lines can be edited without changing saved entries.
|
|
60
|
+
History includes messages and slash commands, skips blank lines and consecutive
|
|
61
|
+
duplicates, and keeps the latest 100 entries in memory for the current pwn-mesh
|
|
62
|
+
session only; it is not written to disk.
|
|
63
|
+
|
|
64
|
+
### Encrypted DMs and key discovery
|
|
65
|
+
|
|
66
|
+
On serial, Bluetooth, or TCP, a DM checks the destination's public key in the
|
|
67
|
+
connected radio's NodeInfo records. If missing, pwn-mesh sends its own public
|
|
68
|
+
NodeInfo to that destination with a response requested and waits up to 15 seconds.
|
|
69
|
+
Only after obtaining the destination key does it submit the text with PKI
|
|
70
|
+
encryption explicitly required. Both radios must be reachable over the selected
|
|
71
|
+
channel; this does not verify a peer's identity outside Meshtastic's key exchange.
|
|
72
|
+
|
|
73
|
+
Discovery failure sends no DM text. The most recent failed attempt is retained
|
|
74
|
+
in session memory; bare `/msg` retries it using the current connection and active
|
|
75
|
+
channel. A new DM replaces that retained attempt, and exiting the process loses
|
|
76
|
+
it. Retention ends after transport submission, not a delivery acknowledgement;
|
|
77
|
+
later radio failures are displayed separately. There is no plaintext or
|
|
78
|
+
channel-PSK downgrade. Missing local public-key metadata requires a reconnect.
|
|
79
|
+
|
|
80
|
+
MQTT channel broadcasts remain supported, but PKI DMs are not implemented for
|
|
81
|
+
the broker transport. MQTT DM attempts remain unsent with an explicit error;
|
|
82
|
+
switch to serial, Bluetooth, or TCP and use `/msg` to retry.
|
|
83
|
+
|
|
57
84
|
## Multi-line input
|
|
58
85
|
|
|
59
86
|
`pwn-ai` and `pwn-asm` use a custom `PWNMultiLineInput` reader. Plain
|
|
@@ -270,11 +270,15 @@ module PWN
|
|
|
270
270
|
next unless ch.is_a?(Hash)
|
|
271
271
|
|
|
272
272
|
role = (ch[:role] || ch['role']).to_s.downcase
|
|
273
|
-
next if %w[disabled 0].include?(role)
|
|
274
|
-
|
|
275
273
|
idx = (ch[:index] || ch['index'] || 0).to_i
|
|
276
274
|
next unless (0..7).cover?(idx)
|
|
277
275
|
|
|
276
|
+
# Protobuf omits the default DISABLED role from to_h.
|
|
277
|
+
if role.empty? || %w[disabled 0].include?(role)
|
|
278
|
+
by_index.delete(idx)
|
|
279
|
+
next
|
|
280
|
+
end
|
|
281
|
+
|
|
278
282
|
settings = ch[:settings] || ch['settings'] || {}
|
|
279
283
|
settings = settings.to_h if !settings.is_a?(Hash) && settings.respond_to?(:to_h)
|
|
280
284
|
settings = {} unless settings.is_a?(Hash)
|
|
@@ -674,6 +678,65 @@ module PWN
|
|
|
674
678
|
end
|
|
675
679
|
end
|
|
676
680
|
|
|
681
|
+
def mesh_node_user(opts = {})
|
|
682
|
+
obj = opts[:obj]
|
|
683
|
+
return unless obj.is_a?(Hash)
|
|
684
|
+
|
|
685
|
+
rows = obj[:rx_mutex] ? obj[:rx_mutex].synchronize { Array(obj[:proto_data]).dup } : Array(obj[:proto_data]).dup
|
|
686
|
+
user = nil
|
|
687
|
+
rows.each do |row|
|
|
688
|
+
next unless row.is_a?(Hash)
|
|
689
|
+
|
|
690
|
+
info = row[:node_info] || row[:nodeInfo]
|
|
691
|
+
user = info[:user] if info.is_a?(Hash) && info[:num] == opts[:num]
|
|
692
|
+
packet = row[:packet]
|
|
693
|
+
next unless packet.is_a?(Hash) && packet[:from] == opts[:num]
|
|
694
|
+
|
|
695
|
+
data = packet[:decoded]
|
|
696
|
+
next unless data.is_a?(Hash) && %w[4 NODEINFO_APP].include?(data[:portnum].to_s)
|
|
697
|
+
|
|
698
|
+
payload = data[:payload]
|
|
699
|
+
user = payload.is_a?(String) ? Meshtastic::User.decode(payload).to_h : payload
|
|
700
|
+
end
|
|
701
|
+
user if user.is_a?(Hash)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def mesh_dm_key(opts = {})
|
|
705
|
+
kind = opts[:kind]
|
|
706
|
+
raise IOError, 'MQTT PKI DMs are unsupported; message remains unsent. Use a device transport.' if kind == :mqtt
|
|
707
|
+
|
|
708
|
+
obj = opts[:obj]
|
|
709
|
+
raise IOError, 'No connected radio; message remains unsent.' unless obj.is_a?(Hash)
|
|
710
|
+
|
|
711
|
+
num = opts[:to].delete_prefix('!').to_i(16)
|
|
712
|
+
user = mesh_node_user(obj: obj, num: num)
|
|
713
|
+
return user[:public_key] if user && user[:public_key].to_s.bytesize == 32
|
|
714
|
+
|
|
715
|
+
own = mesh_node_user(obj: obj, num: obj[:my_node_num])
|
|
716
|
+
raise IOError, 'Local radio public key unavailable; reconnect to refresh NodeInfo. Message remains unsent.' unless own && own[:public_key].to_s.bytesize == 32
|
|
717
|
+
|
|
718
|
+
timeout = Float(opts.fetch(:timeout, 15))
|
|
719
|
+
raise ArgumentError, 'key timeout must be between 0 and 60 seconds' unless timeout.finite? && (0..60).cover?(timeout)
|
|
720
|
+
|
|
721
|
+
mesh_ui_puts(text: "Discovering public key for #{opts[:to]} (up to #{timeout}s); DM text is held locally.")
|
|
722
|
+
data = Meshtastic::Data.new(portnum: :NODEINFO_APP, payload: Meshtastic::User.new(own).to_proto, want_response: true)
|
|
723
|
+
transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
|
|
724
|
+
transport.send_data({ "#{kind}_obj": obj, to: opts[:to], channel: opts[:radio] || 0,
|
|
725
|
+
data: data, port_num: Meshtastic::PortNum::NODEINFO_APP, want_ack: true })
|
|
726
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
727
|
+
loop do
|
|
728
|
+
user = mesh_node_user(obj: obj, num: num)
|
|
729
|
+
return user[:public_key] if user && user[:public_key].to_s.bytesize == 32
|
|
730
|
+
|
|
731
|
+
break if obj[:closing] || Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
732
|
+
|
|
733
|
+
sleep 0.05
|
|
734
|
+
end
|
|
735
|
+
raise IOError, 'Destination public key discovery timed out; message remains unsent. Retry with /msg.'
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
private :mesh_node_user, :mesh_dm_key
|
|
739
|
+
|
|
677
740
|
def mesh_send_text(opts = {})
|
|
678
741
|
env = opts[:env] || {}
|
|
679
742
|
obj = opts[:obj]
|
|
@@ -687,38 +750,56 @@ module PWN
|
|
|
687
750
|
radio = opts[:radio]
|
|
688
751
|
radio = mesh_radio_index_for_name(env: env, obj: obj, name: channel_name) if radio.nil? && !channel_name.empty?
|
|
689
752
|
radio = mesh_radio_channel(env: env, obj: obj) if radio.nil? && %i[serial bluetooth tcp].include?(kind)
|
|
753
|
+
dm_key = nil
|
|
754
|
+
unless mesh_broadcast?(to: dest)
|
|
755
|
+
raise ArgumentError, 'DM destination must be ! followed by eight hex digits' unless dest.match?(/\A![0-9a-fA-F]{8}\z/)
|
|
756
|
+
|
|
757
|
+
PWN.send(:remove_const, :MeshPendingDm) if PWN.const_defined?(:MeshPendingDm)
|
|
758
|
+
PWN.const_set(:MeshPendingDm, { to: dest, text: opts[:text].to_s, channel_name: channel_name })
|
|
759
|
+
dm_key = mesh_dm_key(obj: obj, kind: kind, to: dest, radio: radio, timeout: opts.fetch(:key_timeout, 15))
|
|
760
|
+
end
|
|
690
761
|
chunks = mesh_text_chunks(text: opts[:text])
|
|
691
762
|
chunks.each_with_index do |piece, idx|
|
|
692
763
|
since = obj.is_a?(Hash) ? Array(obj[:proto_data]).size : 0
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
tx[:channel] = radio unless radio.nil?
|
|
701
|
-
Meshtastic::Bluetooth.send_text(tx)
|
|
702
|
-
when :tcp
|
|
703
|
-
tx = { tcp_obj: obj, to: dest, text: piece }
|
|
704
|
-
tx[:channel] = radio unless radio.nil?
|
|
705
|
-
Meshtastic::TCP.send_text(tx)
|
|
764
|
+
if dm_key
|
|
765
|
+
bytes = Meshtastic::MeshInterface.new.send_text(from: obj[:my_node_num] || 0, to: dest, channel: radio || 0, text: piece, want_ack: true, psks: nil)
|
|
766
|
+
packet = Meshtastic::ToRadio.decode(bytes)
|
|
767
|
+
packet.packet.pki_encrypted = true
|
|
768
|
+
packet.packet.public_key = dm_key
|
|
769
|
+
transport = { serial: Meshtastic::Serial, bluetooth: Meshtastic::Bluetooth, tcp: Meshtastic::TCP }.fetch(kind)
|
|
770
|
+
transport.send_to_radio({ "#{kind}_obj": obj, to_radio: packet.to_proto })
|
|
706
771
|
else
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
772
|
+
case kind
|
|
773
|
+
when :serial
|
|
774
|
+
tx = { serial_obj: obj, to: dest, text: piece, want_ack: true }
|
|
775
|
+
tx[:channel] = radio unless radio.nil?
|
|
776
|
+
Meshtastic::Serial.send_text(tx)
|
|
777
|
+
when :bluetooth
|
|
778
|
+
tx = { bluetooth_obj: obj, to: dest, text: piece, want_ack: true }
|
|
779
|
+
tx[:channel] = radio unless radio.nil?
|
|
780
|
+
Meshtastic::Bluetooth.send_text(tx)
|
|
781
|
+
when :tcp
|
|
782
|
+
tx = { tcp_obj: obj, to: dest, text: piece }
|
|
783
|
+
tx[:channel] = radio unless radio.nil?
|
|
784
|
+
Meshtastic::TCP.send_text(tx)
|
|
785
|
+
else
|
|
786
|
+
send_psks = psks
|
|
787
|
+
send_psks = mesh_channel_psks(env: env) if send_psks.nil? || send_psks.empty?
|
|
788
|
+
Meshtastic::MQTT.send_text(
|
|
789
|
+
mqtt_obj: obj,
|
|
790
|
+
from: from,
|
|
791
|
+
to: dest,
|
|
792
|
+
region: mesh_mqtt_region(region: opts[:region], env: env),
|
|
793
|
+
topic: mesh_mqtt_topic(env: env, topic: opts[:topic]),
|
|
794
|
+
channel: channel,
|
|
795
|
+
text: piece,
|
|
796
|
+
psks: send_psks
|
|
797
|
+
)
|
|
798
|
+
end
|
|
719
799
|
end
|
|
720
800
|
from_id = from.to_s
|
|
721
801
|
from_id = mesh_self_node_id(env: env, obj: obj) if from_id.empty?
|
|
802
|
+
from_id = mesh_format_node_id(id: from_id)
|
|
722
803
|
PWN.send(:remove_const, :MeshLastTx) if PWN.const_defined?(:MeshLastTx)
|
|
723
804
|
PWN.const_set(:MeshLastTx, { from: from_id, to: dest, text: piece.to_s, at: Time.now })
|
|
724
805
|
mesh_handle_rx(
|
|
@@ -727,6 +808,7 @@ module PWN
|
|
|
727
808
|
msg: {
|
|
728
809
|
packet: {
|
|
729
810
|
channel: radio,
|
|
811
|
+
pki_encrypted: !dm_key.nil?,
|
|
730
812
|
node_id_from: from_id,
|
|
731
813
|
node_id_to: dest,
|
|
732
814
|
decoded: { portnum: :TEXT_MESSAGE_APP, payload: piece.to_s }
|
|
@@ -738,6 +820,10 @@ module PWN
|
|
|
738
820
|
|
|
739
821
|
mesh_wait_tx_slot(obj: obj, since: since)
|
|
740
822
|
end
|
|
823
|
+
return unless dm_key && PWN.const_defined?(:MeshPendingDm)
|
|
824
|
+
|
|
825
|
+
PWN.send(:remove_const, :MeshPendingDm)
|
|
826
|
+
PWN.const_set(:MeshPendingDm, nil)
|
|
741
827
|
end
|
|
742
828
|
|
|
743
829
|
# Close the Meshtastic session opened by #mesh_connect.
|
|
@@ -830,6 +916,10 @@ module PWN
|
|
|
830
916
|
end
|
|
831
917
|
text = +''
|
|
832
918
|
cursor = 0
|
|
919
|
+
history = []
|
|
920
|
+
history_index = 0
|
|
921
|
+
draft = +''
|
|
922
|
+
draft_cursor = 0
|
|
833
923
|
while pi.config.pwn_mesh
|
|
834
924
|
mesh_drain_events
|
|
835
925
|
mesh_draw_input(text: text, cursor: cursor)
|
|
@@ -839,10 +929,33 @@ module PWN
|
|
|
839
929
|
break if text.empty?
|
|
840
930
|
when "\n", "\r", 10, 13, Curses::KEY_ENTER
|
|
841
931
|
submitted = text.dup
|
|
932
|
+
unless submitted.strip.empty? || history.last == submitted
|
|
933
|
+
history << submitted.dup
|
|
934
|
+
history.shift if history.length > 100
|
|
935
|
+
end
|
|
936
|
+
history_index = history.length
|
|
937
|
+
draft = +''
|
|
938
|
+
draft_cursor = 0
|
|
842
939
|
text.clear
|
|
843
940
|
cursor = 0
|
|
844
941
|
mesh_draw_input(text: text, cursor: cursor)
|
|
845
942
|
mesh_submit(request: submitted, pry: pi)
|
|
943
|
+
when Curses::KEY_UP
|
|
944
|
+
next if history.empty? || history_index.zero?
|
|
945
|
+
|
|
946
|
+
if history_index == history.length
|
|
947
|
+
draft = text.dup
|
|
948
|
+
draft_cursor = cursor
|
|
949
|
+
end
|
|
950
|
+
history_index -= 1
|
|
951
|
+
text = history[history_index].dup
|
|
952
|
+
cursor = text.length
|
|
953
|
+
when Curses::KEY_DOWN
|
|
954
|
+
next if history_index >= history.length
|
|
955
|
+
|
|
956
|
+
history_index += 1
|
|
957
|
+
text = history_index == history.length ? draft.dup : history[history_index].dup
|
|
958
|
+
cursor = history_index == history.length ? draft_cursor : text.length
|
|
846
959
|
when "\u007f", "\b", 127, 8, Curses::KEY_BACKSPACE
|
|
847
960
|
if cursor.positive?
|
|
848
961
|
cursor -= 1
|
|
@@ -918,7 +1031,7 @@ module PWN
|
|
|
918
1031
|
end
|
|
919
1032
|
end
|
|
920
1033
|
win.setpos(3, 2)
|
|
921
|
-
win.addstr('Enter send Tab complete /menu settings Ctrl+D back'[0, win.maxx - 4])
|
|
1034
|
+
win.addstr('Enter send Up/Down history Tab complete /menu settings Ctrl+D back'[0, win.maxx - 4])
|
|
922
1035
|
win.refresh
|
|
923
1036
|
end
|
|
924
1037
|
|
|
@@ -1199,12 +1312,24 @@ module PWN
|
|
|
1199
1312
|
''
|
|
1200
1313
|
end
|
|
1201
1314
|
|
|
1315
|
+
def mesh_format_node_id(opts = {})
|
|
1316
|
+
id = opts[:id]
|
|
1317
|
+
return format('!%08x', id) if id.is_a?(Integer) && (0..0xffffffff).cover?(id)
|
|
1318
|
+
|
|
1319
|
+
text = id.to_s
|
|
1320
|
+
return text unless text.match?(/\A![0-9a-fA-F]{1,8}\z/)
|
|
1321
|
+
|
|
1322
|
+
format('!%08x', text.delete_prefix('!').to_i(16))
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
private :mesh_format_node_id
|
|
1326
|
+
|
|
1202
1327
|
def mesh_self_node_id(opts = {})
|
|
1203
1328
|
return '!00000b0b' unless opts.is_a?(Hash)
|
|
1204
1329
|
|
|
1205
1330
|
obj = opts[:obj]
|
|
1206
1331
|
obj = PWN.const_get(:MeshObj) if obj.nil? && PWN.const_defined?(:MeshObj)
|
|
1207
|
-
return
|
|
1332
|
+
return mesh_format_node_id(id: obj[:my_node_num].to_i) if obj.is_a?(Hash) && !obj[:my_node_num].nil?
|
|
1208
1333
|
|
|
1209
1334
|
'!00000b0b'
|
|
1210
1335
|
end
|
|
@@ -1212,7 +1337,7 @@ module PWN
|
|
|
1212
1337
|
def mesh_decorate_local_id(opts = {})
|
|
1213
1338
|
return '' unless opts.is_a?(Hash)
|
|
1214
1339
|
|
|
1215
|
-
id = opts[:id]
|
|
1340
|
+
id = mesh_format_node_id(id: opts[:id])
|
|
1216
1341
|
return id if id.empty?
|
|
1217
1342
|
|
|
1218
1343
|
self_id = mesh_self_node_id(env: opts[:env], obj: opts[:obj])
|
|
@@ -1225,7 +1350,7 @@ module PWN
|
|
|
1225
1350
|
return true unless opts.is_a?(Hash)
|
|
1226
1351
|
|
|
1227
1352
|
psk = opts[:psk].to_s.strip
|
|
1228
|
-
psk.empty? || %w[none default aq==].include?(psk.downcase)
|
|
1353
|
+
psk.empty? || %w[none default aq==].include?(psk.downcase) || psk == '1PG7OiApB1nwvP+rz05pAQ=='
|
|
1229
1354
|
end
|
|
1230
1355
|
|
|
1231
1356
|
def mesh_channel_securely_encrypted?(opts = {})
|
|
@@ -1281,7 +1406,17 @@ module PWN
|
|
|
1281
1406
|
|
|
1282
1407
|
packet = msg[:packet].is_a?(Hash) ? msg[:packet] : msg
|
|
1283
1408
|
decoded = packet[:decoded]
|
|
1284
|
-
return unless decoded.is_a?(Hash)
|
|
1409
|
+
return unless decoded.is_a?(Hash)
|
|
1410
|
+
|
|
1411
|
+
if %w[5 ROUTING_APP].include?(decoded[:portnum].to_s)
|
|
1412
|
+
routing = decoded[:payload]
|
|
1413
|
+
routing = Meshtastic::Routing.decode(routing).to_h if routing.is_a?(String)
|
|
1414
|
+
reason = routing[:error_reason] if routing.is_a?(Hash)
|
|
1415
|
+
reason = Meshtastic::Routing::Error.lookup(reason) || reason if reason.is_a?(Integer)
|
|
1416
|
+
mesh_ui_puts(text: "TX failed: packet #{decoded[:request_id]}: #{reason}") if reason && !%w[0 NONE].include?(reason.to_s)
|
|
1417
|
+
return
|
|
1418
|
+
end
|
|
1419
|
+
return unless mesh_text_app?(portnum: decoded[:portnum])
|
|
1285
1420
|
|
|
1286
1421
|
env = mesh_env_hash
|
|
1287
1422
|
idx = packet[:channel] || packet['channel']
|
|
@@ -1300,9 +1435,11 @@ module PWN
|
|
|
1300
1435
|
return if rx_text.strip.empty?
|
|
1301
1436
|
|
|
1302
1437
|
from_id = packet[:node_id_from].to_s
|
|
1303
|
-
from_id =
|
|
1438
|
+
from_id = packet[:from] if from_id.empty? && packet[:from]
|
|
1439
|
+
from_id = mesh_format_node_id(id: from_id)
|
|
1304
1440
|
to = packet[:node_id_to].to_s
|
|
1305
|
-
to =
|
|
1441
|
+
to = packet[:to] if to.empty? && packet[:to]
|
|
1442
|
+
to = mesh_format_node_id(id: to)
|
|
1306
1443
|
unless opts[:local]
|
|
1307
1444
|
last = PWN.const_defined?(:MeshLastTx) ? PWN.const_get(:MeshLastTx) : nil
|
|
1308
1445
|
if last.is_a?(Hash) &&
|
|
@@ -1332,13 +1469,15 @@ module PWN
|
|
|
1332
1469
|
state = PWN.const_defined?(:MeshRxState) ? PWN.const_get(:MeshRxState) : {}
|
|
1333
1470
|
ts = Time.now.strftime('%H:%M:%S')
|
|
1334
1471
|
color = opts[:local] ? 23 : 21
|
|
1335
|
-
|
|
1472
|
+
secure = packet[:pki_encrypted] == true || mesh_channel_securely_encrypted?(env: env, channel: channel_name)
|
|
1473
|
+
security_icon = secure ? '🔒' : '🔍'
|
|
1474
|
+
current_line = "#{ts} #{security_icon} #{from.strip} #{dest_label}\n#{rx_text}"
|
|
1336
1475
|
unless state[:last_line] == current_line
|
|
1337
1476
|
rx_body_win = PWN.const_get(:MeshRxBodyWin)
|
|
1338
1477
|
mutex.synchronize do
|
|
1339
1478
|
width = [rx_body_win.maxx - 2, 1].max
|
|
1340
1479
|
rx_body_win.attron(Curses.color_pair(color) | Curses::A_BOLD)
|
|
1341
|
-
rx_body_win.addstr(" #{ts} #{from.strip} · #{dest_label}\n")
|
|
1480
|
+
rx_body_win.addstr(" #{ts} #{security_icon} #{from.strip} · #{dest_label}\n")
|
|
1342
1481
|
rx_body_win.attroff(Curses.color_pair(color) | Curses::A_BOLD)
|
|
1343
1482
|
rx_body_win.attron(Curses.color_pair(24))
|
|
1344
1483
|
mesh_wrap_text(text: rx_text, width: width - 1).each do |line|
|
|
@@ -1669,11 +1808,16 @@ module PWN
|
|
|
1669
1808
|
def pwn_mesh_run_msg(opts = {})
|
|
1670
1809
|
env = opts[:env] || mesh_env_hash
|
|
1671
1810
|
tokens = Array(opts[:args]).map(&:to_s)
|
|
1811
|
+
if tokens.empty? && PWN.const_defined?(:MeshPendingDm) && PWN::MeshPendingDm
|
|
1812
|
+
pending = PWN::MeshPendingDm
|
|
1813
|
+
tokens = [pending[:to], pending[:text]]
|
|
1814
|
+
end
|
|
1672
1815
|
names = mesh_channel_names(env: env)
|
|
1673
1816
|
dest = nil
|
|
1674
1817
|
channel_name = ''
|
|
1675
1818
|
if tokens[0].to_s.match?(/\A![0-9a-fA-F]{8}\z/)
|
|
1676
1819
|
dest = tokens.shift
|
|
1820
|
+
channel_name = env.dig(:channel, :active).to_s
|
|
1677
1821
|
elsif names.any? { |n| n.casecmp?(tokens[0].to_s) }
|
|
1678
1822
|
channel_name = names.find { |n| n.casecmp?(tokens[0].to_s) }.to_s
|
|
1679
1823
|
tokens.shift
|
|
@@ -1688,6 +1832,7 @@ module PWN
|
|
|
1688
1832
|
raise ArgumentError, 'usage: /msg [!nodeid|channel] <text>' if dest.to_s.empty? || text.empty?
|
|
1689
1833
|
|
|
1690
1834
|
channel_name = PWN.const_get(:MeshLastChannel).to_s if channel_name.empty? && PWN.const_defined?(:MeshLastChannel)
|
|
1835
|
+
channel_name = env.dig(:channel, :active).to_s if channel_name.empty?
|
|
1691
1836
|
ch = env[:channel] || {}
|
|
1692
1837
|
slot = ch[channel_name.to_sym] || ch[channel_name] || {}
|
|
1693
1838
|
obj = PWN.const_defined?(:MeshObj) ? PWN.const_get(:MeshObj) : nil
|
data/lib/pwn/version.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
4
5
|
|
|
5
6
|
describe PWN::Plugins::REPL, 'mesh packet channel routing' do
|
|
6
7
|
let(:mesh_env) do
|
|
@@ -42,6 +43,18 @@ describe PWN::Plugins::REPL, 'mesh packet channel routing' do
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
it 'does not route LongFast to disabled protobuf slots with omitted roles' do
|
|
47
|
+
mesh_env[:channel][:LongFast].delete(:radio_index)
|
|
48
|
+
radio[:proto_data] = [
|
|
49
|
+
Meshtastic::FromRadio.new(channel: Meshtastic::Channel.new(index: 1, role: :SECONDARY, settings: { name: 'LongFast' })).to_h,
|
|
50
|
+
Meshtastic::FromRadio.new(channel: Meshtastic::Channel.new(index: 7, role: :DISABLED)).to_h
|
|
51
|
+
]
|
|
52
|
+
expect(described_class.send(:mesh_radio_index_for_name, env: mesh_env, obj: radio, name: 'LongFast')).to eq(1)
|
|
53
|
+
expect(described_class.send(:mesh_device_channel_meta, obj: radio).keys).to eq([1])
|
|
54
|
+
radio[:proto_data] << Meshtastic::FromRadio.new(channel: Meshtastic::Channel.new(index: 1, role: :DISABLED)).to_h
|
|
55
|
+
expect(described_class.send(:mesh_device_channel_meta, obj: radio)).to be_empty
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
it 'paints and replies on the packet radio slot instead of the selected channel' do
|
|
46
59
|
win = double('rx', maxx: 80)
|
|
47
60
|
allow(win).to receive(:attron)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
4
5
|
|
|
5
6
|
describe PWN::Plugins::REPL, 'mesh msg command' do
|
|
6
7
|
let(:mesh_env) do
|
|
@@ -51,6 +52,22 @@ describe PWN::Plugins::REPL, 'mesh msg command' do
|
|
|
51
52
|
expect(described_class.pwn_mesh_complete(target: '/', line: '/')).not_to include('/dm')
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
it 'encodes a named MQTT channel message before receiving anything' do
|
|
56
|
+
broker = double('broker', client_id: '!00000b0b')
|
|
57
|
+
PWN.send(:remove_const, :MeshObj)
|
|
58
|
+
PWN.const_set(:MeshObj, broker)
|
|
59
|
+
envelope = nil
|
|
60
|
+
allow(broker).to receive(:publish) do |_topic, bytes|
|
|
61
|
+
envelope = Meshtastic::ServiceEnvelope.decode(bytes)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
described_class.send(:pwn_mesh_run_msg, env: mesh_env, args: %w[LongFast hello])
|
|
65
|
+
|
|
66
|
+
expect(envelope.packet.to).to eq(0xffffffff)
|
|
67
|
+
expect(envelope.packet.channel).to eq(8)
|
|
68
|
+
expect(envelope.channel_id).to eq('LongFast')
|
|
69
|
+
end
|
|
70
|
+
|
|
54
71
|
it 'addresses a named channel with /msg <channel> text' do
|
|
55
72
|
mesh_env[:channel][:LongFast] = { psk: 'cHdu', radio_index: 1, topic: '2/e/LongFast/#' }
|
|
56
73
|
allow(described_class).to receive(:mesh_send_text)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
5
|
+
|
|
6
|
+
describe PWN::Plugins::REPL, 'mesh DM transport replay' do
|
|
7
|
+
it 'encodes /msg on the active slot and paints a decoded receiver packet through Serial.subscribe' do
|
|
8
|
+
env = { transport: 'serial', channel: { active: 'LongFast', LongFast: { radio_index: 1, psk: 'AQ==' } } }
|
|
9
|
+
sender = { my_node_num: 0xaabbccdd, proto_data: [{ node_info: { num: 0xbbccddee, user: { public_key: 'b' * 32 } } }] }
|
|
10
|
+
stub_const('PWN::MeshPendingDm', nil)
|
|
11
|
+
receiver = { my_node_num: 0xbbccddee, from_radio_queue: Queue.new }
|
|
12
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
13
|
+
stub_const('PWN::MeshObj', sender)
|
|
14
|
+
stub_const('PWN::MeshLastChannel', '')
|
|
15
|
+
stub_const('PWN::MeshLastTx', nil)
|
|
16
|
+
stub_const('PWN::MeshLastDm', nil)
|
|
17
|
+
allow(described_class).to receive(:mesh_env_hash).and_return(env)
|
|
18
|
+
packet = nil
|
|
19
|
+
allow(Meshtastic::Serial).to receive(:send_to_radio) do |opts|
|
|
20
|
+
packet = Meshtastic::ToRadio.decode(opts[:to_radio]).packet
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
described_class.pwn_mesh_dispatch_slash!(request: '/msg !bbccddee hello receiver', env: env, pry: :fixture)
|
|
24
|
+
expect(packet.to).to eq(receiver[:my_node_num])
|
|
25
|
+
expect(packet.channel).to eq(1)
|
|
26
|
+
expect(packet.decoded.payload).to eq('hello receiver')
|
|
27
|
+
|
|
28
|
+
# Replay the firmware's decoded FromRadio boundary, not RF delivery.
|
|
29
|
+
receiver[:from_radio_queue] << Meshtastic::FromRadio.decode(Meshtastic::FromRadio.new(packet: packet).to_proto)
|
|
30
|
+
receiver[:from_radio_queue].close
|
|
31
|
+
PWN.send(:remove_const, :MeshObj)
|
|
32
|
+
PWN.const_set(:MeshObj, receiver)
|
|
33
|
+
PWN.send(:remove_const, :MeshLastTx)
|
|
34
|
+
PWN.const_set(:MeshLastTx, nil)
|
|
35
|
+
win = double('conversation', maxx: 60, attron: nil, attroff: nil, addstr: nil, refresh: nil)
|
|
36
|
+
stub_const('PWN::MeshRxBodyWin', win)
|
|
37
|
+
stub_const('PWN::MeshMutex', Mutex.new)
|
|
38
|
+
stub_const('PWN::MeshRxState', {})
|
|
39
|
+
allow(Curses).to receive(:color_pair).and_return(0)
|
|
40
|
+
|
|
41
|
+
described_class.send(
|
|
42
|
+
:mesh_subscribe,
|
|
43
|
+
env: env, obj: receiver, psks: {},
|
|
44
|
+
on_message: proc { |msg| described_class.send(:mesh_handle_rx, msg: msg) }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
expect(win).to have_received(:addstr).with(" hello receiver\n")
|
|
48
|
+
expect(win).to have_received(:addstr).with(a_string_including('!aabbccdd', '!bbccddee (ME)'))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::Plugins::REPL, 'mesh compose history' do
|
|
6
|
+
it 'recalls submissions with Up/Down and restores the unfinished draft and cursor' do
|
|
7
|
+
config = double('config', pwn_mesh: true)
|
|
8
|
+
pry = double('pry', config: config)
|
|
9
|
+
keys = ['first', "\n", '/status', "\n", 'draft', Curses::KEY_LEFT,
|
|
10
|
+
Curses::KEY_UP, Curses::KEY_UP, Curses::KEY_UP,
|
|
11
|
+
Curses::KEY_DOWN, Curses::KEY_DOWN, Curses::KEY_DOWN]
|
|
12
|
+
frames = []
|
|
13
|
+
allow(described_class).to receive(:mesh_drain_events)
|
|
14
|
+
allow(described_class).to receive(:mesh_submit)
|
|
15
|
+
allow(described_class).to receive(:mesh_draw_input) { |opts| frames << [opts[:text].dup, opts[:cursor]] }
|
|
16
|
+
reader = proc do
|
|
17
|
+
allow(config).to receive(:pwn_mesh).and_return(false) if keys.empty?
|
|
18
|
+
keys.shift
|
|
19
|
+
end
|
|
20
|
+
described_class.send(:mesh_console_loop, pry: pry, getch: reader)
|
|
21
|
+
expected = [
|
|
22
|
+
['/status', 7], ['first', 5], ['first', 5],
|
|
23
|
+
['/status', 7], ['draft', 4], ['draft', 4]
|
|
24
|
+
]
|
|
25
|
+
expect(frames.last(6)).to eq(expected)
|
|
26
|
+
expect(described_class).to have_received(:mesh_submit).with(request: 'first', pry: pry)
|
|
27
|
+
expect(described_class).to have_received(:mesh_submit).with(request: '/status', pry: pry)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'edits recalled text without mutating history and skips empty submissions' do
|
|
31
|
+
config = double('config', pwn_mesh: true)
|
|
32
|
+
pry = double('pry', config: config)
|
|
33
|
+
keys = [Curses::KEY_UP, Curses::KEY_DOWN, 'hello', "\n", "\n",
|
|
34
|
+
Curses::KEY_UP, '!', Curses::KEY_DOWN, Curses::KEY_UP]
|
|
35
|
+
frames = []
|
|
36
|
+
allow(described_class).to receive(:mesh_drain_events)
|
|
37
|
+
allow(described_class).to receive(:mesh_submit)
|
|
38
|
+
allow(described_class).to receive(:mesh_draw_input) { |opts| frames << opts[:text].dup }
|
|
39
|
+
reader = proc do
|
|
40
|
+
allow(config).to receive(:pwn_mesh).and_return(false) if keys.empty?
|
|
41
|
+
keys.shift
|
|
42
|
+
end
|
|
43
|
+
described_class.send(:mesh_console_loop, pry: pry, getch: reader)
|
|
44
|
+
expect(frames).to include('hello!')
|
|
45
|
+
expect(frames.last).to eq('hello')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
5
|
+
|
|
6
|
+
describe PWN::Plugins::REPL, 'mesh DM key exchange' do
|
|
7
|
+
let(:local_user) { { id: '!aabbccdd', public_key: 'a' * 32 } }
|
|
8
|
+
let(:remote_user) { { id: '!bbccddee', public_key: 'b' * 32 } }
|
|
9
|
+
let(:obj) { { my_node_num: 0xaabbccdd, proto_data: [{ node_info: { num: 0xaabbccdd, user: local_user } }] } }
|
|
10
|
+
let(:env) { { transport: 'auto', channel: { active: 'LongFast', LongFast: { radio_index: 1, psk: 'AQ==' } } } }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
stub_const('PWN::MeshPendingDm', nil)
|
|
14
|
+
allow(described_class).to receive(:mesh_ui_puts)
|
|
15
|
+
allow(described_class).to receive(:mesh_handle_rx)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
%i[serial bluetooth tcp].each do |kind|
|
|
19
|
+
it "exchanges NodeInfo then sends a PKI-enforced DM over #{kind}" do
|
|
20
|
+
stub_const('PWN::MeshTransport', kind)
|
|
21
|
+
transport = kind == :bluetooth ? Meshtastic::Bluetooth : Meshtastic::Serial
|
|
22
|
+
packets = []
|
|
23
|
+
allow(transport).to receive(:send_to_radio) do |opts|
|
|
24
|
+
packet = Meshtastic::ToRadio.decode(opts[:to_radio]).packet
|
|
25
|
+
packets << packet
|
|
26
|
+
if packet.decoded.portnum == :NODEINFO_APP
|
|
27
|
+
expect(packet.decoded.want_response).to eq(true)
|
|
28
|
+
expect(Meshtastic::User.decode(packet.decoded.payload).public_key).to eq('a' * 32)
|
|
29
|
+
obj[:proto_data] << { packet: { from: 0xbbccddee, decoded: { portnum: 4, payload: Meshtastic::User.new(remote_user).to_proto } } }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
described_class.send(:mesh_send_text, env: env, obj: obj, to: '!bbccddee', text: 'private', channel_name: 'LongFast')
|
|
33
|
+
expect(packets.map { |p| p.decoded.portnum }).to eq(%i[NODEINFO_APP TEXT_MESSAGE_APP])
|
|
34
|
+
expect(packets.last.pki_encrypted).to eq(true)
|
|
35
|
+
expect(packets.last.public_key).to eq('b' * 32)
|
|
36
|
+
expect(packets.last.to).to eq(0xbbccddee)
|
|
37
|
+
expect(PWN::MeshPendingDm).to be_nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'retains the unsent text and never transmits it after discovery times out' do
|
|
42
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
43
|
+
allow(Meshtastic::Serial).to receive(:send_data)
|
|
44
|
+
expect(Meshtastic::Serial).not_to receive(:send_text)
|
|
45
|
+
expect(Meshtastic::Serial).not_to receive(:send_to_radio)
|
|
46
|
+
expect do
|
|
47
|
+
described_class.send(:mesh_send_text, env: env, obj: obj, to: '!bbccddee', text: 'private', key_timeout: 0)
|
|
48
|
+
end.to raise_error(IOError, /public key.*unsent/i)
|
|
49
|
+
expect(PWN::MeshPendingDm[:text]).to eq('private')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'retries the retained message with bare /msg after the key becomes available' do
|
|
53
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
54
|
+
stub_const('PWN::MeshObj', obj)
|
|
55
|
+
stub_const('PWN::MeshPendingDm', { to: '!bbccddee', text: 'retained text' })
|
|
56
|
+
stub_const('PWN::MeshLastTx', nil)
|
|
57
|
+
obj[:proto_data] << { node_info: { num: 0xbbccddee, user: remote_user } }
|
|
58
|
+
expect(Meshtastic::Serial).not_to receive(:send_data)
|
|
59
|
+
expect(Meshtastic::Serial).to receive(:send_to_radio) do |opts|
|
|
60
|
+
packet = Meshtastic::ToRadio.decode(opts[:to_radio]).packet
|
|
61
|
+
expect(packet.pki_encrypted).to eq(true)
|
|
62
|
+
expect(packet.decoded.payload).to eq('retained text')
|
|
63
|
+
end
|
|
64
|
+
described_class.send(:pwn_mesh_run_msg, env: env, args: [])
|
|
65
|
+
expect(PWN::MeshPendingDm).to be_nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'does not use another node public key as the destination key' do
|
|
69
|
+
stub_const('PWN::MeshTransport', :serial)
|
|
70
|
+
obj[:proto_data] << { node_info: { num: 0xccddeeff, user: remote_user } }
|
|
71
|
+
allow(Meshtastic::Serial).to receive(:send_data)
|
|
72
|
+
expect do
|
|
73
|
+
described_class.send(:mesh_send_text, env: env, obj: obj, to: '!bbccddee', text: 'private', key_timeout: 0)
|
|
74
|
+
end.to raise_error(IOError, /timed out/)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'refuses MQTT DMs without publishing a legacy encrypted or plaintext fallback' do
|
|
78
|
+
stub_const('PWN::MeshTransport', :mqtt)
|
|
79
|
+
expect(Meshtastic::MQTT).not_to receive(:send_text)
|
|
80
|
+
expect do
|
|
81
|
+
described_class.send(:mesh_send_text, env: env, obj: double('broker'), to: '!bbccddee', text: 'private')
|
|
82
|
+
end.to raise_error(IOError, /MQTT.*PKI/)
|
|
83
|
+
expect(PWN::MeshPendingDm[:text]).to eq('private')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::Plugins::REPL, 'mesh node ID formatting' do
|
|
6
|
+
it 'preserves broadcast and non-node labels while formatting valid node values' do
|
|
7
|
+
{ 0 => '!00000000', 0xffffffff => '!ffffffff', '!ABCDEF1' => '!0abcdef1',
|
|
8
|
+
'!ffffffff' => '!ffffffff', 'LongFast' => 'LongFast', '!nothex' => '!nothex' }.each do |id, expected|
|
|
9
|
+
expect(described_class.send(:mesh_format_node_id, id: id)).to eq(expected)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'pads the local numeric node ID to eight hex digits' do
|
|
14
|
+
expect(described_class.send(:mesh_self_node_id, obj: { my_node_num: 0xabcdef1 })).to eq('!0abcdef1')
|
|
15
|
+
expect(described_class.send(:mesh_self_node_id, obj: { my_node_num: 0xb0b })).to eq('!00000b0b')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'pads transport-enriched IDs before painting and remembering a DM sender' do
|
|
19
|
+
stub_const('PWN::MeshObj', { my_node_num: 0xb0b })
|
|
20
|
+
stub_const('PWN::MeshLastDm', nil)
|
|
21
|
+
stub_const('PWN::MeshLastTx', nil)
|
|
22
|
+
stub_const('PWN::MeshRxState', {})
|
|
23
|
+
stub_const('PWN::MeshMutex', Mutex.new)
|
|
24
|
+
win = double('conversation', maxx: 80, attron: nil, attroff: nil, addstr: nil, refresh: nil)
|
|
25
|
+
stub_const('PWN::MeshRxBodyWin', win)
|
|
26
|
+
allow(Curses).to receive(:color_pair).and_return(0)
|
|
27
|
+
allow(described_class).to receive(:mesh_maybe_dispatch_to_pwn_ai)
|
|
28
|
+
allow(described_class).to receive(:mesh_env_hash).and_return({ transport: 'mqtt', channel: {} })
|
|
29
|
+
described_class.send(:mesh_handle_rx, msg: { packet: {
|
|
30
|
+
node_id_from: '!abcdef1', node_id_to: '!b0b',
|
|
31
|
+
decoded: { portnum: 1, payload: 'hello' }
|
|
32
|
+
} })
|
|
33
|
+
expect(win).to have_received(:addstr).with(a_string_including('!0abcdef1', '!00000b0b (ME)'))
|
|
34
|
+
expect(PWN::MeshLastDm).to eq('!0abcdef1')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'meshtastic'
|
|
4
5
|
|
|
5
6
|
describe PWN::Plugins::REPL, 'mesh incoming paint' do
|
|
6
7
|
let(:mesh_env) do
|
|
@@ -23,6 +24,28 @@ describe PWN::Plugins::REPL, 'mesh incoming paint' do
|
|
|
23
24
|
PWN::Env[:plugins][:meshtastic] = @prev_mesh if PWN::Env[:plugins].is_a?(Hash)
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
[5, :ROUTING_APP].each do |port|
|
|
28
|
+
it "shows a radio DM failure for routing port #{port} instead of silently dropping it" do
|
|
29
|
+
allow(described_class).to receive(:mesh_ui_puts)
|
|
30
|
+
routing = Meshtastic::Routing.new(error_reason: :PKI_SEND_FAIL_PUBLIC_KEY)
|
|
31
|
+
payload = port == 5 ? routing.to_proto : routing.to_h
|
|
32
|
+
described_class.send(:mesh_handle_rx, msg: {
|
|
33
|
+
packet: { decoded: { portnum: port, request_id: 42, payload: payload } }
|
|
34
|
+
})
|
|
35
|
+
expect(described_class).to have_received(:mesh_ui_puts).with(
|
|
36
|
+
text: a_string_including('TX failed', '42', 'PKI_SEND_FAIL_PUBLIC_KEY')
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'does not label a successful routing response as a transmission failure' do
|
|
42
|
+
allow(described_class).to receive(:mesh_ui_puts)
|
|
43
|
+
described_class.send(:mesh_handle_rx, msg: {
|
|
44
|
+
packet: { decoded: { portnum: 5, request_id: 42, payload: Meshtastic::Routing.new.to_proto } }
|
|
45
|
+
})
|
|
46
|
+
expect(described_class).not_to have_received(:mesh_ui_puts)
|
|
47
|
+
end
|
|
48
|
+
|
|
26
49
|
it 'paints incoming RX in yellow with sender id and channel name or DM destination id' do
|
|
27
50
|
win = double('rx', maxx: 80)
|
|
28
51
|
allow(win).to receive(:attron)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe PWN::Plugins::REPL, 'mesh message security indicators' do
|
|
6
|
+
[true, false].each do |local|
|
|
7
|
+
[
|
|
8
|
+
['private channel', 'cHdu', false, '🔒'],
|
|
9
|
+
['public channel', 'AQ==', false, '🔍'],
|
|
10
|
+
['expanded public key', '1PG7OiApB1nwvP+rz05pAQ==', false, '🔍'],
|
|
11
|
+
['unknown encryption', '', false, '🔍'],
|
|
12
|
+
['PKI packet', 'AQ==', true, '🔒']
|
|
13
|
+
].each do |label, psk, pki, icon|
|
|
14
|
+
it "paints #{icon} for #{local ? 'sent' : 'received'} #{label}" do
|
|
15
|
+
env = { transport: 'mqtt', channel: { active: 'LongFast', LongFast: { psk: psk } } }
|
|
16
|
+
allow(described_class).to receive(:mesh_env_hash).and_return(env)
|
|
17
|
+
allow(described_class).to receive(:mesh_maybe_dispatch_to_pwn_ai)
|
|
18
|
+
stub_const('PWN::MeshLastTx', nil)
|
|
19
|
+
stub_const('PWN::MeshLastChannel', nil)
|
|
20
|
+
stub_const('PWN::MeshMutex', Mutex.new)
|
|
21
|
+
stub_const('PWN::MeshRxState', {})
|
|
22
|
+
win = double('conversation', maxx: 80, attron: nil, attroff: nil, addstr: nil, refresh: nil)
|
|
23
|
+
stub_const('PWN::MeshRxBodyWin', win)
|
|
24
|
+
allow(Curses).to receive(:color_pair).and_return(0)
|
|
25
|
+
described_class.send(
|
|
26
|
+
:mesh_handle_rx,
|
|
27
|
+
local: local, channel_name: 'LongFast',
|
|
28
|
+
msg: { packet: { node_id_from: '!aabbccdd', node_id_to: '!ffffffff', pki_encrypted: pki,
|
|
29
|
+
decoded: { portnum: 1, payload: 'security test' } } }
|
|
30
|
+
)
|
|
31
|
+
expect(win).to have_received(:addstr).with(a_string_including(icon, '!aabbccdd', 'LongFast'))
|
|
32
|
+
expect(Curses).to have_received(:color_pair).with(local ? 23 : 21).twice
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -497,7 +497,7 @@ describe PWN::Plugins::REPL do # rubocop:disable Metrics/BlockLength
|
|
|
497
497
|
|
|
498
498
|
it 'uses the named device channel index instead of forcing slot zero' do
|
|
499
499
|
require 'meshtastic'
|
|
500
|
-
obj = { proto_data: [{ channel: { index: 3, settings: { name: 'LongFast' } } }] }
|
|
500
|
+
obj = { proto_data: [{ channel: { index: 3, role: :SECONDARY, settings: { name: 'LongFast' } } }] }
|
|
501
501
|
env = { transport: 'serial', channel: { active: 'LongFast', LongFast: { channel_num: 99 } } }
|
|
502
502
|
expect(Meshtastic::Serial).to receive(:send_text).with(hash_including(channel: 3))
|
|
503
503
|
described_class.send(:mesh_send_text, env: env, obj: obj, channel: 99, text: 'slot three')
|
|
@@ -507,8 +507,8 @@ describe PWN::Plugins::REPL do # rubocop:disable Metrics/BlockLength
|
|
|
507
507
|
require 'meshtastic'
|
|
508
508
|
obj = {
|
|
509
509
|
proto_data: [
|
|
510
|
-
{ channel: { index: 0, settings: { name: '' } } },
|
|
511
|
-
{ channel: { index: 2, settings: { name: 'LongFast' } } }
|
|
510
|
+
{ channel: { index: 0, role: :PRIMARY, settings: { name: '' } } },
|
|
511
|
+
{ channel: { index: 2, role: :SECONDARY, settings: { name: 'LongFast' } } }
|
|
512
512
|
]
|
|
513
513
|
}
|
|
514
514
|
env = {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pwn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.737
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 0day Inc.
|
|
@@ -6915,8 +6915,13 @@ files:
|
|
|
6915
6915
|
- spec/lib/pwn/plugins/repl_mesh_console_spec.rb
|
|
6916
6916
|
- spec/lib/pwn/plugins/repl_mesh_dispatch_spec.rb
|
|
6917
6917
|
- spec/lib/pwn/plugins/repl_mesh_dm_spec.rb
|
|
6918
|
+
- spec/lib/pwn/plugins/repl_mesh_dm_transport_spec.rb
|
|
6919
|
+
- spec/lib/pwn/plugins/repl_mesh_history_spec.rb
|
|
6920
|
+
- spec/lib/pwn/plugins/repl_mesh_key_exchange_spec.rb
|
|
6918
6921
|
- spec/lib/pwn/plugins/repl_mesh_layout_spec.rb
|
|
6922
|
+
- spec/lib/pwn/plugins/repl_mesh_node_id_spec.rb
|
|
6919
6923
|
- spec/lib/pwn/plugins/repl_mesh_rx_paint_spec.rb
|
|
6924
|
+
- spec/lib/pwn/plugins/repl_mesh_security_paint_spec.rb
|
|
6920
6925
|
- spec/lib/pwn/plugins/repl_pwn_vault_spec.rb
|
|
6921
6926
|
- spec/lib/pwn/plugins/repl_spec.rb
|
|
6922
6927
|
- spec/lib/pwn/plugins/rop_spec.rb
|