meshtastic 0.0.40 → 0.0.42

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dc432f90a8545cad1027f5514f0676a60d776f617db87b394436e1657693e98
4
- data.tar.gz: 5a461b32da2516b164ab789f2a27653b5f46a4f7d36020ddca1ba161931162e0
3
+ metadata.gz: 3ffe0a1e8528090281aedce5b132f1f57aa82db0c8c46131cff53d00004e4dcd
4
+ data.tar.gz: 56bb82c58cdcbced0c401802180931aa4625e4314e4954ab30349811a614d399
5
5
  SHA512:
6
- metadata.gz: 1094355139b2e703dda03bf60ff7034aeb94e1d7d335ba8cd73aae0d9c3bae4c64e61cfb3b8e2dcbdd8894174ab5e3f77b29fde04617fedc3f578b5e469a2678
7
- data.tar.gz: d5677d5b337b4f1b8cbff41da88eafaeab8377f3235085f6ba2df384b7c19dd24cbece877d1d6c39f4cf3c4b9d6674f93ed6cb783bc62bd739e12379f3a8a552
6
+ metadata.gz: dd84aa7e0607992de00be53a0f8ccb2d69077004dd21ebc2838be38582064a797c0f13317c0abd375ab67199dd494640187895384ea74110c77c9b478a218654
7
+ data.tar.gz: a1135c741f44f38765421ea2020173b40395478fffdbb24da760d75f8483920f99cf2f0d110f7173fb9617e7522a644dc418f937aef79a35f1b04d89ed94737c
data/Gemfile CHANGED
@@ -13,7 +13,7 @@ gemspec
13
13
  # to review these custom flags (e.g. pg, serialport, etc).
14
14
  gem 'bundler', '>=2.5.3'
15
15
  gem 'bundle-audit', '0.1.0'
16
- gem 'geocoder', '1.8.2'
16
+ gem 'geocoder', '1.8.3'
17
17
  gem 'google-protobuf', '3.21.12'
18
18
  gem 'mqtt', '0.6.0'
19
19
  gem 'rake', '13.2.1'
@@ -21,6 +21,7 @@ gem 'rdoc', '6.6.3.1'
21
21
  gem 'rspec', '3.13.0'
22
22
  gem 'rubocop', '1.63.4'
23
23
  gem 'rubocop-rake', '0.6.0'
24
- gem 'rubocop-rspec', '2.29.1'
24
+ gem 'rubocop-rspec', '2.29.2'
25
25
  gem 'rvm', '1.11.3.9'
26
+ gem 'tty-prompt', '0.23.1'
26
27
  gem 'yard', '0.9.36'
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'mqtt'
7
7
  require 'openssl'
8
8
  require 'securerandom'
9
+ require 'tty-prompt'
9
10
 
10
11
  # Avoiding Namespace Collisions
11
12
  MQTTClient = MQTT::Client
@@ -42,16 +43,156 @@ module Meshtastic
42
43
  raise e
43
44
  end
44
45
 
46
+ # Supported Method Parameters::
47
+ # Meshtastic::MQQT.get_cipher_keys(
48
+ # psks: 'required - hash of channel / pre-shared key value pairs'
49
+ # )
50
+
51
+ private_class_method def self.get_cipher_keys(opts = {})
52
+ psks = opts[:psks]
53
+
54
+ psks.each_key do |key|
55
+ psk = psks[key]
56
+ padded_psk = psk.ljust(psk.length + ((4 - (psk.length % 4)) % 4), '=')
57
+ replaced_psk = padded_psk.gsub('-', '+').gsub('_', '/')
58
+ psks[key] = replaced_psk
59
+ end
60
+
61
+ psks
62
+ rescue StandardError => e
63
+ raise e
64
+ end
65
+
66
+ # Supported Method Parameters::
67
+ # Meshtastic::MQQT.recursively_decode_payloads(
68
+ # object: 'required - object to recursively decode',
69
+ # msg_type: 'required - message type (e.g. :TEXT_MESSAGE_APP)',
70
+ # gps_metadata: 'optional - include GPS metadata in output (default: false)'
71
+ # )
72
+
73
+ private_class_method def self.recursively_decode_payloads(opts = {})
74
+ object = opts[:object]
75
+ msg_type = opts[:msg_type]
76
+ gps_metadata = opts[:gps_metadata]
77
+
78
+ # puts "OBJECT: #{object.inspect}"
79
+ object = Meshtastic::Data.decode(object).to_h if object.is_a?(String)
80
+
81
+ object.each do |key, value|
82
+ if value.is_a?(Hash)
83
+ # Recursive call if the value is a hash
84
+ recursively_decode_payloads(object: value)
85
+ elsif value.is_a?(Array)
86
+ # Process each element if it's an array
87
+ value.each do |item|
88
+ recursively_decode_payloads(object: item) if item.is_a?(Hash)
89
+ end
90
+ end
91
+ # Check if the key matches 'payload' and decode
92
+ case key
93
+ when :latitude_i
94
+ object[:latitude] = object[:latitude_i] * 0.0000001
95
+ when :longitude_i
96
+ object[:longitude] = object[:longitude_i] * 0.0000001
97
+ when :macaddr
98
+ object[key] = object[key].bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join(':')
99
+ when :payload
100
+ case msg_type
101
+ when :ADMIN_APP
102
+ decoder = Meshtastic::AdminMessage.decode(payload)
103
+ when :ATAK_FORWARDER, :ATAK_PLUGIN
104
+ decoder = Meshtastic::TAKPacket
105
+ # when :AUDIO_APP
106
+ # decoder = Meshtastic::Audio
107
+ when :DETECTION_SENSOR_APP
108
+ decoder = Meshtastic::DeviceState
109
+ # when :IP_TUNNEL_APP
110
+ # decoder = Meshtastic::IpTunnel
111
+ when :MAP_REPORT_APP
112
+ decoder = Meshtastic::MapReport
113
+ # when :MAX
114
+ # decoder = Meshtastic::Max
115
+ when :NEIGHBORINFO_APP
116
+ decoder = Meshtastic::NeighborInfo
117
+ when :NODEINFO_APP
118
+ decoder = Meshtastic::User
119
+ when :PAXCOUNTER_APP
120
+ decoder = Meshtastic::Paxcount
121
+ when :POSITION_APP
122
+ decoder = Meshtastic::Position
123
+ # when :PRIVATE_APP
124
+ # decoder = Meshtastic::Private
125
+ when :RANGE_TEST_APP
126
+ # Unsure if this is the correct protobuf object
127
+ decoder = Meshtastic::FromRadio
128
+ when :REMOTE_HARDWARE_APP
129
+ decoder = Meshtastic::HardwareMessage
130
+ # when :REPLY_APP
131
+ # decoder = Meshtastic::Reply
132
+ when :ROUTING_APP
133
+ decoder = Meshtastic::Routing
134
+ when :SERIAL_APP
135
+ decoder = Meshtastic::SerialConnectionStatus
136
+ when :SIMULATOR_APP
137
+ decoder = Meshtastic::Compressed
138
+ when :STORE_FORWARD_APP
139
+ decoder = Meshtastic::StoreAndForward
140
+ when :TEXT_MESSAGE_APP
141
+ # Unsure if this is the correct protobuf object
142
+ # decoder = Meshtastic::MqttClientProxyMessage
143
+ decoder = Meshtastic::Data
144
+ when :TELEMETRY_APP
145
+ decoder = Meshtastic::Telemetry
146
+ when :TRACEROUTE_APP
147
+ decoder = Meshtastic::RouteDiscovery
148
+ when :UNKNOWN_APP
149
+ decoder = Meshtastic::Data.decode
150
+ when :WAYPOINT_APP
151
+ decoder = Meshtastic::Waypoint
152
+ # when :ZPS_APP
153
+ # decoder = Meshtastic::Zps
154
+ else
155
+ puts "WARNING: Unknown message type: #{msg_type}"
156
+ decoder = Meshtastic::Data.decode
157
+ end
158
+
159
+ # begin
160
+ # # If the value is base64 encoded, base64 decode and then decode
161
+ # base64_decoded_value = Base64.strict_decode64(value)
162
+ # object[key] = decoder.decode(base64_decoded_value).to_h
163
+ # rescue ArgumentError => e
164
+ # # Otherwise, just decode the value
165
+ object[key] = decoder.decode(value).to_h
166
+ # end
167
+
168
+ end
169
+
170
+ next unless gps_metadata && object[:latitude] && object[:longitude]
171
+
172
+ object[:gps_metadata] = gps_search(
173
+ lat: object[:latitude],
174
+ lon: object[:longitude]
175
+ ).first.data
176
+ end
177
+
178
+ object
179
+ rescue Google::Protobuf::ParseError
180
+ object
181
+ rescue StandardError => e
182
+ raise e
183
+ end
184
+
45
185
  # Supported Method Parameters::
46
186
  # Meshtastic::MQQT.subscribe(
47
187
  # mqtt_obj: 'required - mqtt_obj returned from #connect method'
48
188
  # root_topic: 'optional - root topic (default: msh)',
49
189
  # region: 'optional - region e.g. 'US/VA', etc (default: US)',
50
190
  # channel: 'optional - channel name e.g. "2/stat/#" (default: "2/e/LongFast/#")',
51
- # psk: 'optional - channel pre-shared key (default: AQ==)',
191
+ # psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: "AQ==" })',
52
192
  # qos: 'optional - quality of service (default: 0)',
53
193
  # filter: 'optional - comma-delimited string(s) to filter on in message (default: nil)',
54
- # gps_metadata: 'optional - include GPS metadata in output (default: false)'
194
+ # gps_metadata: 'optional - include GPS metadata in output (default: false)',
195
+ # include_raw: 'optional - include raw packet data in output (default: false)'
55
196
  # )
56
197
 
57
198
  public_class_method def self.subscribe(opts = {})
@@ -60,34 +201,30 @@ module Meshtastic
60
201
  region = opts[:region] ||= 'US'
61
202
  channel = opts[:channel] ||= '2/e/LongFast/#'
62
203
  # TODO: Support Array of PSKs and attempt each until decrypted
63
- psk = opts[:psk] ||= 'AQ=='
204
+
205
+ public_psk = '1PG7OiApB1nwvP+rz05pAQ=='
206
+ psks = opts[:psks] ||= { LongFast: public_psk }
207
+ raise 'ERROR: psks parameter must be a hash of :channel => psk key value pairs' unless psks.is_a?(Hash)
208
+
209
+ psks[:LongFast] = public_psk if psks[:LongFast] == 'AQ=='
210
+ psks = get_cipher_keys(psks: psks)
211
+
64
212
  qos = opts[:qos] ||= 0
65
213
  json = opts[:json] ||= false
66
214
  filter = opts[:filter]
67
215
  gps_metadata = opts[:gps_metadata] ||= false
216
+ include_raw = opts[:include_raw] ||= false
68
217
 
69
- # TODO: Find JSON URI for this
218
+ # NOTE: Use MQTT Explorer for topic discovery
70
219
  full_topic = "#{root_topic}/#{region}/#{channel}"
71
220
  puts "Subscribing to: #{full_topic}"
72
221
  mqtt_obj.subscribe(full_topic, qos)
73
222
 
74
- # Decrypt the message
75
- # Our AES key is 128 or 256 bits, shared as part of the 'Channel' specification.
76
-
77
- # Actual pre-shared key for LongFast channel
78
- psk = '1PG7OiApB1nwvP+rz05pAQ==' if psk == 'AQ=='
79
- padded_psk = psk.ljust(psk.length + ((4 - (psk.length % 4)) % 4), '=')
80
- replaced_psk = padded_psk.gsub('-', '+').gsub('_', '/')
81
- psk = replaced_psk
82
- dec_psk = Base64.strict_decode64(psk)
83
-
84
- # cipher = OpenSSL::Cipher.new('AES-256-CTR')
85
- cipher = OpenSSL::Cipher.new('AES-128-CTR')
86
223
  filter_arr = filter.to_s.split(',').map(&:strip)
87
224
  mqtt_obj.get_packet do |packet_bytes|
88
- # raw_packet = packet_bytes.to_s.b
225
+ raw_packet = packet_bytes.to_s.b if include_raw
89
226
  raw_topic = packet_bytes.topic ||= ''
90
- raw_payload = packet_bytes.payload
227
+ raw_payload = packet_bytes.payload ||= ''
91
228
 
92
229
  begin
93
230
  disp = false
@@ -107,116 +244,45 @@ module Meshtastic
107
244
  message[:node_id_from] = "!#{message[:from].to_i.to_s(16)}"
108
245
  message[:node_id_to] = "!#{message[:to].to_i.to_s(16)}"
109
246
 
247
+ # If encrypted_message is not nil, then decrypt
248
+ # the message prior to decoding.
110
249
  encrypted_message = message[:encrypted]
111
- # If encrypted_message is not nil, then decrypt the message
112
250
  if encrypted_message.to_s.length.positive?
113
251
  packet_id = message[:id]
114
252
  packet_from = message[:from]
253
+
115
254
  nonce_packet_id = [packet_id].pack('V').ljust(8, "\x00")
116
255
  nonce_from_node = [packet_from].pack('V').ljust(8, "\x00")
117
256
  nonce = "#{nonce_packet_id}#{nonce_from_node}".b
118
257
 
119
- # Decrypt the message
120
- # Key must be 32 bytes
121
- # IV mustr be 16 bytes
258
+ psk = psks[:LongFast]
259
+ target_chanel = decoded_payload_hash[:channel_id].to_s.to_sym
260
+ psk = psks[target_chanel] if psks.keys.include?(target_chanel)
261
+ dec_psk = Base64.strict_decode64(psk)
262
+
263
+ cipher = OpenSSL::Cipher.new('AES-128-CTR')
264
+ cipher = OpenSSL::Cipher.new('AES-256-CTR') if dec_psk.length == 32
122
265
  cipher.decrypt
123
266
  cipher.key = dec_psk
124
267
  cipher.iv = nonce
125
268
 
126
269
  decrypted = cipher.update(encrypted_message) + cipher.final
127
- message[:decrypted] = decrypted
270
+ message[:decoded] = Meshtastic::Data.decode(decrypted).to_h
271
+ message[:encrypted] = :decrypted
128
272
  end
129
273
 
130
274
  if message[:decoded]
275
+ # payload = Meshtastic::Data.decode(message[:decoded][:payload]).to_h
131
276
  payload = message[:decoded][:payload]
132
-
133
277
  msg_type = message[:decoded][:portnum]
134
- case msg_type
135
- when :ADMIN_APP
136
- pb_obj = Meshtastic::AdminMessage.decode(payload)
137
- when :ATAK_FORWARDER, :ATAK_PLUGIN
138
- pb_obj = Meshtastic::TAKPacket.decode(payload)
139
- # when :AUDIO_APP
140
- # pb_obj = Meshtastic::Audio.decode(payload)
141
- when :DETECTION_SENSOR_APP
142
- pb_obj = Meshtastic::DeviceState.decode(payload)
143
- # when :IP_TUNNEL_APP
144
- # pb_obj = Meshtastic::IpTunnel.decode(payload)
145
- when :MAP_REPORT_APP
146
- pb_obj = Meshtastic::MapReport.decode(payload)
147
- # when :MAX
148
- # pb_obj = Meshtastic::Max.decode(payload)
149
- when :NEIGHBORINFO_APP
150
- pb_obj = Meshtastic::NeighborInfo.decode(payload)
151
- when :NODEINFO_APP
152
- pb_obj = Meshtastic::User.decode(payload)
153
- when :PAXCOUNTER_APP
154
- pb_obj = Meshtastic::Paxcount.decode(payload)
155
- when :POSITION_APP
156
- pb_obj = Meshtastic::Position.decode(payload)
157
- # when :PRIVATE_APP
158
- # pb_obj = Meshtastic::Private.decode(payload)
159
- when :RANGE_TEST_APP
160
- # Unsure if this is the correct protobuf object
161
- pb_obj = Meshtastic::FromRadio.decode(payload)
162
- when :REMOTE_HARDWARE_APP
163
- pb_obj = Meshtastic::HardwareMessage.decode(payload)
164
- # when :REPLY_APP
165
- # pb_obj = Meshtastic::Reply.decode(payload)
166
- when :ROUTING_APP
167
- pb_obj = Meshtastic::Routing.decode(payload)
168
- when :SERIAL_APP
169
- pb_obj = Meshtastic::SerialConnectionStatus.decode(payload)
170
- when :SIMULATOR_APP,
171
- :TEXT_MESSAGE_COMPRESSED_APP
172
- # Unsure if this is the correct protobuf object
173
- # for TEXT_MESSAGE_COMPRESSED_APP
174
- pb_obj = Meshtastic::Compressed.decode(payload)
175
- when :STORE_FORWARD_APP
176
- pb_obj = Meshtastic::StoreAndForward.decode(payload)
177
- when :TEXT_MESSAGE_APP
178
- # Unsure if this is the correct protobuf object
179
- # pb_obj = Meshtastic::MqttClientProxyMessage.decode(payload)
180
- pb_obj = Meshtastic::Data.decode(payload)
181
- when :TELEMETRY_APP
182
- pb_obj = Meshtastic::Telemetry.decode(payload)
183
- when :TRACEROUTE_APP
184
- pb_obj = Meshtastic::RouteDiscovery.decode(payload)
185
- # when :UNKNOWN_APP
186
- # pb_obj = Meshtastic.Unknown.decode(payload)
187
- when :WAYPOINT_APP
188
- pb_obj = Meshtastic::Waypoint.decode(payload)
189
- # when :ZPS_APP
190
- # pb_obj = Meshtastic::Zps.decode(payload)
191
- else
192
- puts "WARNING: Unknown message type: #{msg_type}"
193
- end
194
- # Overwrite the payload with the decoded protobuf object
195
- # message[:decoded][:payload] = pb_obj.to_h unless msg_type == :TRACEROUTE_APP
196
- message[:decoded][:payload] = pb_obj.to_h
197
- if message[:decoded][:payload].keys.include?(:latitude_i) &&
198
- message[:decoded][:payload].keys.include?(:longitude_i) &&
199
- gps_metadata
200
-
201
- latitude = pb_obj.to_h[:latitude_i] * 0.0000001
202
- longitude = pb_obj.to_h[:longitude_i] * 0.0000001
203
- message[:decoded][:payload][:gps_metadata] = gps_search(
204
- lat: latitude,
205
- lon: longitude
206
- ).first.data
207
- end
208
-
209
- # If we there's a mac address, make it look like one.
210
- if message[:decoded][:payload].keys.include?(:macaddr)
211
- macaddr = message[:decoded][:payload][:macaddr]
212
- macaddr_fmt = macaddr.bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join(':')
213
- message[:decoded][:payload][:macaddr] = macaddr_fmt
214
- end
215
- # puts pb_obj.public_methods
216
- # message[:decoded][:pb_obj] = pb_obj
278
+ message[:decoded][:payload] = recursively_decode_payloads(
279
+ object: payload,
280
+ msg_type: msg_type,
281
+ gps_metadata: gps_metadata
282
+ )
217
283
  end
218
284
 
219
- # message[:raw_packet] = raw_packet if block_given?
285
+ message[:raw_packet] = raw_packet if include_raw
220
286
  decoded_payload_hash[:packet] = message
221
287
  unless block_given?
222
288
  message[:stdout] = 'pretty'
@@ -227,8 +293,11 @@ module Meshtastic
227
293
  ArgumentError => e
228
294
 
229
295
  message[:decrypted] = e.message if e.message.include?('key must be')
296
+ message[:decrypted] = 'unable to decrypt - psk?' if e.message.include?('occurred during parsing')
230
297
  decoded_payload_hash[:packet] = message
231
298
  unless block_given?
299
+ puts "WARNING: #{e.inspect} - MSG IS >>>"
300
+ # puts e.backtrace
232
301
  message[:stdout] = 'inspect'
233
302
  stdout_message = decoded_payload_hash.inspect
234
303
  end
@@ -320,7 +389,7 @@ module Meshtastic
320
389
  root_topic: 'optional - root topic (default: msh)',
321
390
  region: 'optional - region e.g. 'US/VA', etc (default: US)',
322
391
  channel: 'optional - channel name e.g. '2/stat/#' (default: '2/e/LongFast/#')',
323
- psk: 'optional - channel pre-shared key (default: AQ==)',
392
+ psks: 'optional - hash of :channel => psk key value pairs (default: { LongFast: 'AQ==' })',
324
393
  qos: 'optional - quality of service (default: 0)',
325
394
  json: 'optional - JSON output (default: false)',
326
395
  filter: 'optional - comma-delimited string(s) to filter on in message (default: nil)',
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Meshtastic
4
- VERSION = '0.0.40'
4
+ VERSION = '0.0.42'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: meshtastic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.40
4
+ version: 0.0.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-01 00:00:00.000000000 Z
11
+ date: 2024-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 1.8.2
47
+ version: 1.8.3
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 1.8.2
54
+ version: 1.8.3
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: google-protobuf
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - '='
158
158
  - !ruby/object:Gem::Version
159
- version: 2.29.1
159
+ version: 2.29.2
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - '='
165
165
  - !ruby/object:Gem::Version
166
- version: 2.29.1
166
+ version: 2.29.2
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: rvm
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - '='
179
179
  - !ruby/object:Gem::Version
180
180
  version: 1.11.3.9
181
+ - !ruby/object:Gem::Dependency
182
+ name: tty-prompt
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - '='
186
+ - !ruby/object:Gem::Version
187
+ version: 0.23.1
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - '='
193
+ - !ruby/object:Gem::Version
194
+ version: 0.23.1
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: yard
183
197
  requirement: !ruby/object:Gem::Requirement