pwn 0.5.256 → 0.5.257

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.
@@ -1,14 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
4
+ require 'timeout'
5
+
3
6
  module PWN
4
7
  module Plugins
5
8
  # This plugin is used for interacting with a three track
6
9
  # MSR206 Magnetic Stripe Reader / Writer
7
10
  module MSR206
11
+ # Logger instance for auditing and debugging
12
+ private_class_method def self.logger
13
+ @logger ||= Logger.new('msr206.log')
14
+ end
15
+
8
16
  # Supported Method Parameters::
9
17
  # msr206_obj = PWN::Plugins::MSR206.connect(
10
18
  # block_dev: 'optional - serial block device path (defaults to /dev/ttyUSB0)',
11
- # baud: 'optional - (defaults to 9600)',
19
+ # baud: 'optional - (defaults to 19200)',
12
20
  # data_bits: 'optional - (defaults to 8)',
13
21
  # stop_bits: 'optional - (defaults to 1)',
14
22
  # parity: 'optional - :even|:mark|:odd|:space|:none (defaults to :none),'
@@ -16,24 +24,27 @@ module PWN
16
24
  # )
17
25
 
18
26
  public_class_method def self.connect(opts = {})
19
- # Default Baud Rate for this Device is 19200
20
- opts[:block_dev] = '/dev/ttyUSB0' unless opts[:block_dev]
21
- opts[:baud] = 9_600 unless opts[:baud]
22
- opts[:data_bits] = 8 unless opts[:data_bits]
23
- opts[:stop_bits] = 1 unless opts[:stop_bits]
24
- opts[:parity] = :none unless opts[:parity]
25
- opts[:flow_control] = :soft unless opts[:flow_control]
27
+ opts[:block_dev] ||= '/dev/ttyUSB0'
28
+ opts[:baud] ||= 19_200 # Align with device default
29
+ opts[:data_bits] ||= 8
30
+ opts[:stop_bits] ||= 1
31
+ opts[:parity] ||= :none
32
+ opts[:flow_control] ||= :soft
33
+
34
+ logger.info("Connecting to #{opts[:block_dev]} at baud #{opts[:baud]}")
26
35
  msr206_obj = PWN::Plugins::Serial.connect(opts)
36
+ set_protocol(msr206_obj: msr206_obj, protocol: :usi0) # Default to USI0
37
+ msr206_obj
27
38
  rescue StandardError => e
39
+ logger.error("Connection failed: #{e.message}")
28
40
  disconnect(msr206_obj: msr206_obj) unless msr206_obj.nil?
29
41
  raise e
30
42
  end
31
43
 
32
44
  # Supported Method Parameters::
33
- # cmds = PWN::Plugins::MSR206.list_cmds
45
+ # cmds = PWN::Plugins::MSR206.list_cmds
34
46
  public_class_method def self.list_cmds
35
- # Returns an Array of Symbols
36
- cmds = %i[
47
+ %i[
37
48
  proto_usi0
38
49
  proto_usi1
39
50
  version_report
@@ -92,6 +103,7 @@ module PWN
92
103
  arm_to_write_with_raw_speed_prompts
93
104
  ]
94
105
  rescue StandardError => e
106
+ logger.error("Error listing commands: #{e.message}")
95
107
  raise e
96
108
  end
97
109
 
@@ -102,212 +114,216 @@ module PWN
102
114
 
103
115
  private_class_method def self.decode(opts = {})
104
116
  raw_byte_arr = opts[:raw_byte_arr]
105
-
117
+ parity = opts[:parity] || :none
118
+ parity_not_none = %i[odd even]
106
119
  decoded_data_str = ''
107
- if raw_byte_arr
108
- raw_byte_arr.first.split.each do |byte_str|
109
- # TODO: Different case statements for each parity
110
- case byte_str
111
- when '1B'
112
- decoded_data_str += ''
113
- when '20'
114
- decoded_data_str += ' '
115
- when '21'
116
- decoded_data_str += '!'
117
- when '22'
118
- decoded_data_str += '"'
119
- when '23'
120
- decoded_data_str += '#'
121
- when '24'
122
- decoded_data_str += '$'
123
- when '25'
124
- decoded_data_str += '%'
125
- when '26'
126
- decoded_data_str += '&'
127
- when '27'
128
- decoded_data_str += "'"
129
- when '28'
130
- decoded_data_str += '('
131
- when '29'
132
- decoded_data_str += ')'
133
- when '2A', 'AA'
134
- decoded_data_str += '*'
135
- when '2B', 'AB'
136
- decoded_data_str += '+'
137
- when '2C', 'AC'
138
- decoded_data_str += ','
139
- when '2D', 'AD'
140
- decoded_data_str += '-'
141
- when '2E', 'AE'
142
- decoded_data_str += '.'
143
- when '2F', 'AF'
144
- decoded_data_str += '/'
145
- when '30', 'B0'
146
- decoded_data_str += '0'
147
- when '31', 'B1'
148
- decoded_data_str += '1'
149
- when '32', 'B2'
150
- decoded_data_str += '2'
151
- when '33', 'B3'
152
- decoded_data_str += '3'
153
- when '34', 'B4'
154
- decoded_data_str += '4'
155
- when '35', 'B5'
156
- decoded_data_str += '5'
157
- when '36', 'B6'
158
- decoded_data_str += '6'
159
- when '37', 'B7'
160
- decoded_data_str += '7'
161
- when '38', 'B8'
162
- decoded_data_str += '8'
163
- when '39', 'B9'
164
- decoded_data_str += '9'
165
- when '3A', 'BA'
166
- decoded_data_str += ':'
167
- when '3B', 'BB'
168
- decoded_data_str += ';'
169
- when '3C', 'BC'
170
- decoded_data_str += '<'
171
- when '3D', 'BD'
172
- decoded_data_str += '='
173
- when '3E', 'BE'
174
- decoded_data_str += '>'
175
- when '3F', 'BF'
176
- decoded_data_str += '?'
177
- when '40', 'C0'
178
- decoded_data_str += '@'
179
- when '41', 'C1'
180
- decoded_data_str += 'A'
181
- when '42', 'C2'
182
- decoded_data_str += 'B'
183
- when '43', 'C3'
184
- decoded_data_str += 'C'
185
- when '44', 'C4'
186
- decoded_data_str += 'D'
187
- when '45', 'C5'
188
- decoded_data_str += 'E'
189
- when '46', 'C6'
190
- decoded_data_str += 'F'
191
- when '47', 'C7'
192
- decoded_data_str += 'G'
193
- when '48', 'C8'
194
- decoded_data_str += 'H'
195
- when '49', 'C9'
196
- decoded_data_str += 'I'
197
- when '4A', 'CA'
198
- decoded_data_str += 'J'
199
- when '4B', 'CB'
200
- decoded_data_str += 'K'
201
- when '4C', 'CC'
202
- decoded_data_str += 'L'
203
- when '4D', 'CD'
204
- decoded_data_str += 'M'
205
- when '4E', 'CE'
206
- decoded_data_str += 'N'
207
- when '4F', 'CF'
208
- decoded_data_str += 'O'
209
- when '50', 'D0'
210
- decoded_data_str += 'P'
211
- when '51', 'D1'
212
- decoded_data_str += 'Q'
213
- when '52', 'D2'
214
- decoded_data_str += 'R'
215
- when '53', 'D3'
216
- decoded_data_str += 'S'
217
- when '54', 'D4'
218
- decoded_data_str += 'T'
219
- when '55', 'D5'
220
- decoded_data_str += 'U'
221
- when '56', 'D6'
222
- decoded_data_str += 'V'
223
- when '57', 'D7'
224
- decoded_data_str += 'W'
225
- when '58', 'D8'
226
- decoded_data_str += 'X'
227
- when '59', 'D9'
228
- decoded_data_str += 'Y'
229
- when '5A', 'DA'
230
- decoded_data_str += 'Z'
231
- when '5B', 'DB'
232
- decoded_data_str += '['
233
- when '5C', 'DC'
234
- decoded_data_str += '\\'
235
- when '5D', 'DD'
236
- decoded_data_str += ']'
237
- when '5E', 'DE'
238
- decoded_data_str += '^'
239
- when '5F', 'DF'
240
- decoded_data_str += '_'
241
- when '60', 'E0'
242
- decoded_data_str += '`'
243
- when '61', 'E1'
244
- decoded_data_str += 'a'
245
- when '62', 'E2'
246
- decoded_data_str += 'b'
247
- when '63', 'E3'
248
- decoded_data_str += 'c'
249
- when '64', 'E4'
250
- decoded_data_str += 'd'
251
- when '65', 'E5'
252
- decoded_data_str += 'e'
253
- when '66', 'E6'
254
- decoded_data_str += 'f'
255
- when '67', 'E7'
256
- decoded_data_str += 'g'
257
- when '68', 'E8'
258
- decoded_data_str += 'h'
259
- when '69', 'E9'
260
- decoded_data_str += 'i'
261
- when '6A', 'EA'
262
- decoded_data_str += 'j'
263
- when '6B', 'EB'
264
- decoded_data_str += 'k'
265
- when '6C', 'EC'
266
- decoded_data_str += 'l'
267
- when '6D', 'ED'
268
- decoded_data_str += 'm'
269
- when '6E', 'EE'
270
- decoded_data_str += 'n'
271
- when '6F', 'EF'
272
- decoded_data_str += 'o'
273
- when '70', 'F0'
274
- decoded_data_str += 'p'
275
- when '71', 'F1'
276
- decoded_data_str += 'q'
277
- when '72', 'F2'
278
- decoded_data_str += 'r'
279
- when '73', 'F3'
280
- decoded_data_str += 's'
281
- when '74', 'F4'
282
- decoded_data_str += 't'
283
- when '75', 'F5'
284
- decoded_data_str += 'u'
285
- when '76', 'F6'
286
- decoded_data_str += 'v'
287
- when '77', 'F7'
288
- decoded_data_str += 'w'
289
- when '78', 'F8'
290
- decoded_data_str += 'x'
291
- when '79', 'F9'
292
- decoded_data_str += 'y'
293
- when '7A', 'FA'
294
- decoded_data_str += 'z'
295
- when '7B', 'FB'
296
- decoded_data_str += '{'
297
- when '7C', 'FC'
298
- decoded_data_str += '|'
299
- when '7D', 'FD'
300
- decoded_data_str += '}'
301
- when '7E', 'FE'
302
- decoded_data_str += '~'
303
- else
304
- decoded_data_str += "\u00BF"
305
- end
120
+ return decoded_data_str unless raw_byte_arr
121
+
122
+ raw_byte_arr.first.split.each do |byte_str|
123
+ byte = byte_str.to_i(16)
124
+ # Strip parity bit for odd/even parity
125
+ byte &= 0x7F if parity_not_none.include?(parity)
126
+
127
+ case byte_str
128
+ when '1B'
129
+ decoded_data_str += "\e" # ESC character
130
+ when '20'
131
+ decoded_data_str += ' '
132
+ when '21'
133
+ decoded_data_str += '!'
134
+ when '22'
135
+ decoded_data_str += '"'
136
+ when '23'
137
+ decoded_data_str += '#'
138
+ when '24'
139
+ decoded_data_str += '$'
140
+ when '25'
141
+ decoded_data_str += '%'
142
+ when '26'
143
+ decoded_data_str += '&'
144
+ when '27'
145
+ decoded_data_str += "'"
146
+ when '28'
147
+ decoded_data_str += '('
148
+ when '29'
149
+ decoded_data_str += ')'
150
+ when '2A', 'AA'
151
+ decoded_data_str += '*'
152
+ when '2B', 'AB'
153
+ decoded_data_str += '+'
154
+ when '2C', 'AC'
155
+ decoded_data_str += ','
156
+ when '2D', 'AD'
157
+ decoded_data_str += '-'
158
+ when '2E', 'AE'
159
+ decoded_data_str += '.'
160
+ when '2F', 'AF'
161
+ decoded_data_str += '/'
162
+ when '30', 'B0'
163
+ decoded_data_str += '0'
164
+ when '31', 'B1'
165
+ decoded_data_str += '1'
166
+ when '32', 'B2'
167
+ decoded_data_str += '2'
168
+ when '33', 'B3'
169
+ decoded_data_str += '3'
170
+ when '34', 'B4'
171
+ decoded_data_str += '4'
172
+ when '35', 'B5'
173
+ decoded_data_str += '5'
174
+ when '36', 'B6'
175
+ decoded_data_str += '6'
176
+ when '37', 'B7'
177
+ decoded_data_str += '7'
178
+ when '38', 'B8'
179
+ decoded_data_str += '8'
180
+ when '39', 'B9'
181
+ decoded_data_str += '9'
182
+ when '3A', 'BA'
183
+ decoded_data_str += ':'
184
+ when '3B', 'BB'
185
+ decoded_data_str += ';'
186
+ when '3C', 'BC'
187
+ decoded_data_str += '<'
188
+ when '3D', 'BD'
189
+ decoded_data_str += '='
190
+ when '3E', 'BE'
191
+ decoded_data_str += '>'
192
+ when '3F', 'BF'
193
+ decoded_data_str += '?'
194
+ when '40', 'C0'
195
+ decoded_data_str += '@'
196
+ when '41', 'C1'
197
+ decoded_data_str += 'A'
198
+ when '42', 'C2'
199
+ decoded_data_str += 'B'
200
+ when '43', 'C3'
201
+ decoded_data_str += 'C'
202
+ when '44', 'C4'
203
+ decoded_data_str += 'D'
204
+ when '45', 'C5'
205
+ decoded_data_str += 'E'
206
+ when '46', 'C6'
207
+ decoded_data_str += 'F'
208
+ when '47', 'C7'
209
+ decoded_data_str += 'G'
210
+ when '48', 'C8'
211
+ decoded_data_str += 'H'
212
+ when '49', 'C9'
213
+ decoded_data_str += 'I'
214
+ when '4A', 'CA'
215
+ decoded_data_str += 'J'
216
+ when '4B', 'CB'
217
+ decoded_data_str += 'K'
218
+ when '4C', 'CC'
219
+ decoded_data_str += 'L'
220
+ when '4D', 'CD'
221
+ decoded_data_str += 'M'
222
+ when '4E', 'CE'
223
+ decoded_data_str += 'N'
224
+ when '4F', 'CF'
225
+ decoded_data_str += 'O'
226
+ when '50', 'D0'
227
+ decoded_data_str += 'P'
228
+ when '51', 'D1'
229
+ decoded_data_str += 'Q'
230
+ when '52', 'D2'
231
+ decoded_data_str += 'R'
232
+ when '53', 'D3'
233
+ decoded_data_str += 'S'
234
+ when '54', 'D4'
235
+ decoded_data_str += 'T'
236
+ when '55', 'D5'
237
+ decoded_data_str += 'U'
238
+ when '56', 'D6'
239
+ decoded_data_str += 'V'
240
+ when '57', 'D7'
241
+ decoded_data_str += 'W'
242
+ when '58', 'D8'
243
+ decoded_data_str += 'X'
244
+ when '59', 'D9'
245
+ decoded_data_str += 'Y'
246
+ when '5A', 'DA'
247
+ decoded_data_str += 'Z'
248
+ when '5B', 'DB'
249
+ decoded_data_str += '['
250
+ when '5C', 'DC'
251
+ decoded_data_str += '\\'
252
+ when '5D', 'DD'
253
+ decoded_data_str += ']'
254
+ when '5E', 'DE'
255
+ decoded_data_str += '^'
256
+ when '5F', 'DF'
257
+ decoded_data_str += '_'
258
+ when '60', 'E0'
259
+ decoded_data_str += '`'
260
+ when '61', 'E1'
261
+ decoded_data_str += 'a'
262
+ when '62', 'E2'
263
+ decoded_data_str += 'b'
264
+ when '63', 'E3'
265
+ decoded_data_str += 'c'
266
+ when '64', 'E4'
267
+ decoded_data_str += 'd'
268
+ when '65', 'E5'
269
+ decoded_data_str += 'e'
270
+ when '66', 'E6'
271
+ decoded_data_str += 'f'
272
+ when '67', 'E7'
273
+ decoded_data_str += 'g'
274
+ when '68', 'E8'
275
+ decoded_data_str += 'h'
276
+ when '69', 'E9'
277
+ decoded_data_str += 'i'
278
+ when '6A', 'EA'
279
+ decoded_data_str += 'j'
280
+ when '6B', 'EB'
281
+ decoded_data_str += 'k'
282
+ when '6C', 'EC'
283
+ decoded_data_str += 'l'
284
+ when '6D', 'ED'
285
+ decoded_data_str += 'm'
286
+ when '6E', 'EE'
287
+ decoded_data_str += 'n'
288
+ when '6F', 'EF'
289
+ decoded_data_str += 'o'
290
+ when '70', 'F0'
291
+ decoded_data_str += 'p'
292
+ when '71', 'F1'
293
+ decoded_data_str += 'q'
294
+ when '72', 'F2'
295
+ decoded_data_str += 'r'
296
+ when '73', 'F3'
297
+ decoded_data_str += 's'
298
+ when '74', 'F4'
299
+ decoded_data_str += 't'
300
+ when '75', 'F5'
301
+ decoded_data_str += 'u'
302
+ when '76', 'F6'
303
+ decoded_data_str += 'v'
304
+ when '77', 'F7'
305
+ decoded_data_str += 'w'
306
+ when '78', 'F8'
307
+ decoded_data_str += 'x'
308
+ when '79', 'F9'
309
+ decoded_data_str += 'y'
310
+ when '7A', 'FA'
311
+ decoded_data_str += 'z'
312
+ when '7B', 'FB'
313
+ decoded_data_str += '{'
314
+ when '7C', 'FC'
315
+ decoded_data_str += '|'
316
+ when '7D', 'FD'
317
+ decoded_data_str += '}'
318
+ when '7E', 'FE'
319
+ decoded_data_str += '~'
320
+ else
321
+ decoded_data_str += "\u00BF" # Unknown character
306
322
  end
307
323
  end
308
-
309
324
  decoded_data_str
310
325
  rescue StandardError => e
326
+ logger.error("Error decoding response: #{e.message}")
311
327
  raise e
312
328
  end
313
329
 
@@ -318,16 +334,15 @@ module PWN
318
334
 
319
335
  private_class_method def self.binary(opts = {})
320
336
  raw_byte_arr = opts[:raw_byte_arr]
321
-
322
337
  binary_byte_arr = []
323
338
  if raw_byte_arr
324
339
  raw_byte_arr.first.split.each do |byte_str|
325
340
  binary_byte_arr.push([byte_str].pack('H*').unpack1('B*'))
326
341
  end
327
342
  end
328
-
329
343
  binary_byte_arr
330
344
  rescue StandardError => e
345
+ logger.error("Error converting to binary: #{e.message}")
331
346
  raise e
332
347
  end
333
348
 
@@ -341,99 +356,114 @@ module PWN
341
356
  cmd = opts[:cmd]
342
357
  cmd_bytes = opts[:cmd_bytes]
343
358
 
344
- keep_parsing_responses = true
345
- next_response_detected = false
346
- response = {}
347
- response[:cmd] = cmd
348
- response[:cmd] ||= :na
349
-
350
- if cmd_bytes.instance_of?(Array)
351
- response[:cmd_bytes] = opts[:cmd_bytes].map do |base10_int|
352
- "0x#{base10_int.to_s(16).rjust(2, '0')}"
353
- end
354
- end
355
- response[:cmd_bytes] ||= :na
356
-
357
- raw_byte_arr = []
358
- raw_byte_arr_len = 0
359
- last_raw_byte_arr_len = 0
359
+ Timeout.timeout(5) do # 5-second timeout
360
+ keep_parsing_responses = true
361
+ next_response_detected = false
362
+ response = {}
363
+ response[:cmd] = cmd || :na
364
+ response[:cmd_bytes] = cmd_bytes&.map { |b| "0x#{b.to_s(16).rjust(2, '0')}" } || :na
365
+
366
+ raw_byte_arr = []
367
+ raw_byte_arr_len = 0
368
+ last_raw_byte_arr_len = 0
369
+ cmd_resp = ''
370
+
371
+ while keep_parsing_responses
372
+ until next_response_detected
373
+ last_raw_byte_arr_len = raw_byte_arr_len
374
+ raw_byte_arr = PWN::Plugins::Serial.response(serial_obj: msr206_obj)
375
+ cmd_resp = raw_byte_arr.last
376
+ raw_byte_arr_len = raw_byte_arr.length
377
+ next_response_detected = true if raw_byte_arr_len > last_raw_byte_arr_len
378
+ end
360
379
 
361
- parsed_cmd_resp_arr = []
362
- cmd_resp = ''
380
+ case cmd_resp
381
+ when '21', 'A1'
382
+ response[:msg] = :invalid_command
383
+ when '28', 'A8'
384
+ response[:msg] = :card_speed_measurement_start
385
+ when '29', 'A9'
386
+ response[:msg] = :card_speed_measurement_end
387
+ when '2A', 'AA'
388
+ response[:msg] = :error
389
+ when '2B', 'AB'
390
+ response[:msg] = :no_data_found
391
+ when '2D', 'AD'
392
+ response[:msg] = :insufficient_leading_zeros_for_custom_writing
393
+ when '2F', 'AF'
394
+ response[:msg] = :first_lsb_char_not_one_for_custom_writing
395
+ when '31', 'B1'
396
+ response[:msg] = :unsuccessful_read_after_write_track1
397
+ when '32', 'B2'
398
+ response[:msg] = :unsuccessful_read_after_write_track2
399
+ when '33', 'B3'
400
+ response[:msg] = :unsuccessful_read_after_write_track3
401
+ when '3A', 'BA'
402
+ response[:msg] = :power_on_report
403
+ when '3E', 'BE'
404
+ response[:msg] = :card_edge_detected
405
+ when '3F', 'BF'
406
+ response[:msg] = :communications_error
407
+ when '5E'
408
+ response[:msg] = :ack_command_completed
409
+ when '7E'
410
+ response[:msg] = :command_not_supported_by_hardware
411
+ else
412
+ response[:msg] = :response
413
+ end
363
414
 
364
- while keep_parsing_responses
365
- until next_response_detected
415
+ next_response_detected = false
366
416
  last_raw_byte_arr_len = raw_byte_arr_len
367
- raw_byte_arr = PWN::Plugins::Serial.response(serial_obj: msr206_obj)
368
- cmd_resp = raw_byte_arr.last
369
- raw_byte_arr_len = raw_byte_arr.length
370
-
371
- next_response_detected = true if raw_byte_arr_len > last_raw_byte_arr_len
417
+ keep_parsing_responses = false
372
418
  end
373
419
 
374
- case cmd_resp
375
- when '21', 'A1'
376
- response[:msg] = :invalid_command
377
- when '28', 'A8'
378
- response[:msg] = :card_speed_measurement_start
379
- when '29', 'A9'
380
- response[:msg] = :card_speed_measurement_end
381
- when '2A', 'AA'
382
- response[:msg] = :error
383
- when '2B', 'AB'
384
- response[:msg] = :no_data_found
385
- when '2D', 'AD'
386
- response[:msg] = :insufficient_leading_zeros_for_custom_writing
387
- when '2F', 'AF'
388
- response[:msg] = :first_lsb_char_not_one_for_custom_writing
389
- when '31', 'B1'
390
- response[:msg] = :unsuccessful_read_after_write_track1
391
- when '32', 'B2'
392
- response[:msg] = :unsuccessful_read_after_write_track2
393
- when '33', 'B3'
394
- response[:msg] = :unsuccessful_read_after_write_track3
395
- when '3A', 'BA'
396
- response[:msg] = :power_on_report
397
- when '3E', 'BE'
398
- response[:msg] = :card_edge_detected
399
- when '3F', 'BF'
400
- response[:msg] = :communications_error
401
- when '5E'
402
- response[:msg] = :ack_command_completed
403
- when '7E'
404
- response[:msg] = :command_not_supported_by_hardware
405
- else
406
- response[:msg] = :response
407
- end
408
-
409
- next_response_detected = false
410
- last_raw_byte_arr_len = raw_byte_arr_len
411
- keep_parsing_responses = false
420
+ response[:hex] = raw_byte_arr
421
+ response[:binary] = binary(raw_byte_arr: raw_byte_arr)
422
+ response[:decoded] = decode(raw_byte_arr: raw_byte_arr)
423
+ response
412
424
  end
413
-
414
- response[:hex] = raw_byte_arr
415
- response[:binary] = binary(raw_byte_arr: raw_byte_arr)
416
- response[:decoded] = decode(raw_byte_arr: raw_byte_arr)
417
- response
425
+ rescue Timeout::Error
426
+ logger.error("Device response timed out for command: #{cmd}")
427
+ raise 'ERROR: Device response timed out'
418
428
  rescue StandardError => e
429
+ logger.error("Error parsing response for command #{cmd}: #{e.message}")
419
430
  raise e
420
431
  ensure
421
- # Flush Responses for Next Request
422
432
  PWN::Plugins::Serial.flush_session_data
423
433
  end
424
434
 
425
435
  # Supported Method Parameters::
426
- # PWN::Plugins::MSR206.exec(
436
+ # PWN::Plugins::MSR206.set_protocol(
437
+ # msr206_obj: 'required - msr206_obj returned from #connect method',
438
+ # protocol: 'optional - :usi0 or :usi1 (defaults to :usi0)'
439
+ # )
440
+
441
+ public_class_method def self.set_protocol(opts = {})
442
+ msr206_obj = opts[:msr206_obj]
443
+ protocol = opts[:protocol] || :usi0
444
+ cmd = protocol == :usi0 ? :proto_usi0 : :proto_usi1
445
+ logger.info("Setting protocol to #{protocol}")
446
+ exec(msr206_obj: msr206_obj, cmd: cmd)
447
+ rescue StandardError => e
448
+ logger.error("Error setting protocol: #{e.message}")
449
+ raise e
450
+ end
451
+
452
+ # Supported Method Parameters::
453
+ # PWN::Plugins::MSR206.exec(
427
454
  # msr206_obj: 'required - msr206_obj returned from #connect method'
428
455
  # cmd: 'required - cmd returned from #list_cmds method',
429
456
  # params: 'optional - parameters for specific command returned from #list_params method'
430
457
  # )
458
+
431
459
  public_class_method def self.exec(opts = {})
432
460
  msr206_obj = opts[:msr206_obj]
433
461
  cmd = opts[:cmd].to_s.scrub.strip.chomp
434
462
  params = opts[:params]
435
463
  raise 'ERROR: params argument must be a byte array (e.g. [0x41]).' if params && !params.instance_of?(Array)
436
464
 
465
+ logger.info("Executing command: #{cmd} with params: #{params.inspect}")
466
+
437
467
  params_bytes = []
438
468
  case cmd.to_sym
439
469
  when :proto_usi0
@@ -549,28 +579,27 @@ module PWN
549
579
  when :simulate_power_cycle_warm_reset
550
580
  cmd_bytes = [0x7F]
551
581
  else
552
- raise "Unsupported Command: #{cmd}. Supported commands are:\n#{list_cmds}\n\n\n"
582
+ logger.error("Unsupported command: #{cmd}")
583
+ raise "Unsupported Command: #{cmd}. Supported commands are:\n#{list_cmds.join("\n")}\n"
553
584
  end
554
585
 
555
- # If parameters to a command are set, append them.
556
586
  cmd_bytes += params if params
557
- # Execute the command.
558
587
  PWN::Plugins::Serial.request(
559
588
  serial_obj: msr206_obj,
560
589
  payload: cmd_bytes
561
590
  )
562
591
 
563
- # Parse commands response(s).
564
- # Return an array of hashes.
565
- parse_responses(
592
+ response = parse_responses(
566
593
  msr206_obj: msr206_obj,
567
594
  cmd: cmd.to_sym,
568
595
  cmd_bytes: cmd_bytes
569
596
  )
597
+ logger.info("Response for #{cmd}: #{response.inspect}")
598
+ response
570
599
  rescue StandardError => e
600
+ logger.error("Error executing command #{cmd}: #{e.message}")
571
601
  raise e
572
602
  ensure
573
- # Flush Responses for Next Request
574
603
  PWN::Plugins::Serial.flush_session_data
575
604
  end
576
605
 
@@ -588,45 +617,35 @@ module PWN
588
617
  encoding = opts[:encoding].to_s.scrub.strip.chomp.to_sym
589
618
  track_data = opts[:track_data]
590
619
 
591
- exec_resp = exec(
592
- msr206_obj: msr206_obj,
593
- cmd: :red_off
594
- )
620
+ logger.info("Waiting for swipe: type=#{type}, encoding=#{encoding}")
595
621
 
596
- exec_resp = exec(
597
- msr206_obj: msr206_obj,
598
- cmd: :yellow_off
599
- )
600
-
601
- exec_resp = exec(
602
- msr206_obj: msr206_obj,
603
- cmd: :green_on
604
- )
622
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :red_off)
623
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :yellow_off)
624
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :green_on)
605
625
 
606
626
  track_data_arr = []
607
627
 
608
- case type
609
- when :arm_to_read,
610
- :arm_to_read_w_speed_prompts
628
+ # Check for card edge detection
629
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :card_edge_detect)
630
+ unless exec_resp[:msg] == :card_edge_detected
631
+ logger.warn('Card not detected')
632
+ puts 'WARNING: Card not detected. Please ensure card is ready.'
633
+ end
611
634
 
612
- exec_resp = PWN::Plugins::MSR206.exec(
613
- msr206_obj: msr206_obj,
614
- cmd: type
615
- )
616
- puts exec_resp.inspect
635
+ case type
636
+ when :arm_to_read, :arm_to_read_w_speed_prompts
637
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: type)
638
+ logger.info("Arm to read response: #{exec_resp.inspect}")
617
639
 
618
- print 'Reader Activated. Please Swipe Card...'
640
+ print 'Reader Activated. Please Swipe Card...'
619
641
  loop do
620
- exec_resp = parse_responses(
621
- msr206_obj: msr206_obj,
622
- cmd: type
623
- )
624
-
642
+ exec_resp = parse_responses(msr206_obj: msr206_obj, cmd: type)
625
643
  puts exec_resp[:msg]
626
644
  break if exec_resp[:msg] == :ack_command_completed
627
645
  end
628
646
 
629
- if encoding == :iso
647
+ case encoding
648
+ when :iso
630
649
  cmds_arr = %i[
631
650
  tx_iso_std_data_track1
632
651
  tx_iso_std_data_track2
@@ -634,33 +653,23 @@ module PWN
634
653
  ]
635
654
  cmds_arr.each do |cmd|
636
655
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
637
- exec_resp = exec(
638
- msr206_obj: msr206_obj,
639
- cmd: cmd
640
- )
656
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd)
641
657
  exec_resp[:encoding] = encoding
642
658
  puts exec_resp[:decoded]
643
659
  puts exec_resp.inspect
644
660
  track_data_arr.push(exec_resp)
645
661
  end
646
- end
647
-
648
- if encoding == :iso_alt
662
+ when :iso_alt
649
663
  cmds_arr = %i[
650
664
  alt_tx_iso_std_data_track1
651
665
  alt_tx_iso_std_data_track2
652
666
  alt_tx_iso_std_data_track3
653
667
  ]
654
-
655
668
  cmds_arr.each do |cmd|
656
669
  params_arr = [0x31, 0x32, 0x33]
657
670
  params_arr.each do |param|
658
671
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
659
- exec_resp = exec(
660
- msr206_obj: msr206_obj,
661
- cmd: cmd,
662
- params: [param]
663
- )
672
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: [param])
664
673
  exec_resp[:encoding] = encoding
665
674
  exec_resp[:track_format] = [param]
666
675
  puts exec_resp[:decoded]
@@ -668,38 +677,25 @@ module PWN
668
677
  track_data_arr.push(exec_resp)
669
678
  end
670
679
  end
671
- end
672
-
673
- if encoding == :raw
680
+ when :raw
674
681
  cmds_arr = %i[
675
682
  tx_custom_data_forward_track1
676
683
  tx_custom_data_forward_track2
677
684
  tx_custom_data_forward_track3
678
685
  ]
679
-
680
686
  cmds_arr.each do |cmd|
681
687
  params_arr = [0x33, 0x34, 0x35, 0x36, 0x37]
682
688
  params_arr.each do |param|
683
689
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
684
- # 2 byte command
685
- exec_resp = exec(
686
- msr206_obj: msr206_obj,
687
- cmd: cmd,
688
- params: [param]
689
- )
690
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: [param])
690
691
  exec_resp[:encoding] = encoding
691
692
  exec_resp[:track_format] = [param]
692
693
  puts exec_resp[:decoded]
693
694
  puts exec_resp.inspect
694
695
  track_data_arr.push(exec_resp)
695
696
 
696
- # 3 byte command
697
697
  param = [0x5f] + [param]
698
- exec_resp = exec(
699
- msr206_obj: msr206_obj,
700
- cmd: cmd,
701
- params: param
702
- )
698
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: param)
703
699
  exec_resp[:encoding] = encoding
704
700
  exec_resp[:track_format] = param
705
701
  puts exec_resp[:decoded]
@@ -707,26 +703,27 @@ module PWN
707
703
  track_data_arr.push(exec_resp)
708
704
  end
709
705
  end
706
+ else
707
+ logger.error("Unsupported encoding: #{encoding}")
708
+ raise "Unsupported encoding: #{encoding}"
710
709
  end
711
- when :arm_to_write_no_raw,
712
- :arm_to_write_with_raw,
713
- :arm_to_write_with_raw_speed_prompts
714
710
 
715
- # TODO: Set Write Density for Tracks Here
716
- # >>>
711
+ when :arm_to_write_no_raw, :arm_to_write_with_raw, :arm_to_write_with_raw_speed_prompts
712
+ unless track_data.is_a?(Array) && track_data.all? { |t| t.is_a?(Hash) && t.key?(:decoded) }
713
+ logger.error('Invalid track_data: must be an array of hashes with :decoded key')
714
+ raise 'Invalid track_data: must be an array of hashes with :decoded key'
715
+ end
717
716
 
718
- if encoding == :iso
717
+ case encoding
718
+ when :iso
719
719
  cmds_arr = %i[
720
720
  load_iso_std_data_for_writing_track1
721
721
  load_iso_std_data_for_writing_track2
722
722
  load_iso_std_data_for_writing_track3
723
723
  ]
724
-
725
- # TODO: Get Data by cmd (e.g. load_iso_std_data_for_writing_track1)
726
724
  cmds_arr.each_with_index do |cmd, track|
727
725
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
728
- puts track_data[track][:decoded]
729
- next if track_data[track][:decoded] == '+'
726
+ # next unless track_data[track]&.key?(:decoded) && track_data[track][:decoded]&.strip&.length&.positive?
730
727
 
731
728
  this_track = track_data[track][:decoded].chars.map do |c|
732
729
  c.unpack1('H*').to_i(16)
@@ -734,106 +731,79 @@ module PWN
734
731
  track_eot = [0x04]
735
732
  track_payload = this_track + track_eot
736
733
  puts track_payload.inspect
737
- exec_resp = exec(
738
- msr206_obj: msr206_obj,
739
- cmd: cmd,
740
- params: track_payload
741
- )
734
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: track_payload)
742
735
  exec_resp[:encoding] = encoding
743
736
  puts exec_resp.inspect
744
737
  track_data_arr.push(exec_resp)
745
738
  end
746
- end
747
-
748
- if encoding == :iso_alt
739
+ when :iso_alt
749
740
  cmds_arr = %i[
750
741
  alt_load_iso_std_data_for_writing_track1
751
742
  alt_load_iso_std_data_for_writing_track2
752
743
  alt_load_iso_std_data_for_writing_track3
753
744
  ]
754
-
755
- # TODO: Get Data by cmd (e.g. alt_load_iso_std_data_for_writing_track1)
756
745
  cmds_arr.each_with_index do |cmd, track|
757
746
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
758
- puts track_data[track][:decoded]
759
- next if track_data[track][:decoded] == '+'
747
+ # next unless track_data[track]&.key?(:decoded) && track_data[track][:decoded]&.strip&.length&.positive?
760
748
 
761
749
  this_track = track_data[track][:decoded].chars.map do |c|
762
750
  c.unpack1('H*').to_i(16)
763
751
  end
764
- track_format = track_data[track][:track_format]
752
+ track_format = track_data[track][:track_format] || [0x31]
765
753
  track_eot = [0x04]
766
754
  track_payload = track_format + this_track + track_eot
767
755
  puts track_payload.inspect
768
- exec_resp = exec(
769
- msr206_obj: msr206_obj,
770
- cmd: cmd,
771
- params: track_payload
772
- )
756
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: track_payload)
773
757
  exec_resp[:encoding] = encoding
774
758
  puts exec_resp.inspect
775
759
  track_data_arr.push(exec_resp)
776
760
  end
777
- end
778
-
779
- if encoding == :raw
761
+ when :raw
780
762
  cmds_arr = %i[
781
763
  load_custom_data_for_writing_track1
782
764
  load_custom_data_for_writing_track2
783
765
  load_custom_data_for_writing_track3
784
766
  ]
785
-
786
- # TODO: Get Data by cmd (e.g. load_custom_data_for_writing_track1)
787
767
  cmds_arr.each_with_index do |cmd, track|
788
768
  puts "\n*** #{cmd.to_s.gsub('_', ' ').upcase} #{'*' * 17}"
789
- puts track_data[track][:decoded]
790
- next if track_data[track][:decoded] == '+'
769
+ # next unless track_data[track]&.key?(:decoded) && track_data[track][:decoded]&.strip&.length&.positive?
791
770
 
792
771
  this_track = track_data[track][:decoded].chars.map do |c|
793
772
  c.unpack1('H*').to_i(16)
794
773
  end
795
- track_format = track_data[track][:track_format]
774
+ track_format = track_data[track][:track_format] || [0x33]
796
775
  track_eot = [0x04]
797
776
  track_payload = track_format + this_track + track_eot
798
777
  puts track_payload.inspect
799
- exec_resp = exec(
800
- msr206_obj: msr206_obj,
801
- cmd: cmd,
802
- params: track_payload
803
- )
778
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: cmd, params: track_payload)
804
779
  exec_resp[:encoding] = encoding
805
780
  puts exec_resp.inspect
806
781
  track_data_arr.push(exec_resp)
807
782
  end
783
+ else
784
+ logger.error("Unsupported encoding: #{encoding}")
785
+ raise "Unsupported encoding: #{encoding}"
808
786
  end
809
787
 
810
- exec_resp = PWN::Plugins::MSR206.exec(
811
- msr206_obj: msr206_obj,
812
- cmd: type
813
- )
814
- puts exec_resp.inspect
788
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: type)
789
+ logger.info("Arm to write response: #{exec_resp.inspect}")
815
790
 
816
- print 'Writer Activated. Please Swipe Card...'
791
+ print 'Writer Activated. Please Swipe Card...'
817
792
  loop do
818
- exec_resp = parse_responses(
819
- msr206_obj: msr206_obj,
820
- cmd: type
821
- )
822
-
793
+ exec_resp = parse_responses(msr206_obj: msr206_obj, cmd: type)
823
794
  break if exec_resp[:msg] == :ack_command_completed
824
795
  end
825
796
  else
826
- raise "ERROR Unsupported type in #wait_for_swipe - #{type}"
797
+ logger.error("Unsupported swipe type: #{type}")
798
+ raise "Unsupported type in #wait_for_swipe: #{type}"
827
799
  end
828
800
 
829
801
  track_data_arr
830
802
  rescue StandardError => e
803
+ logger.error("Error in wait_for_swipe: #{e.message}")
831
804
  raise e
832
805
  ensure
833
- exec_resp = exec(
834
- msr206_obj: msr206_obj,
835
- cmd: :green_off
836
- )
806
+ exec(msr206_obj: msr206_obj, cmd: :green_off)
837
807
  end
838
808
 
839
809
  # Supported Method Parameters::
@@ -843,7 +813,7 @@ module PWN
843
813
 
844
814
  public_class_method def self.read_card(opts = {})
845
815
  msr206_obj = opts[:msr206_obj]
846
- type = opts[:type].to_s.scrub.strip.chomp.to_sym
816
+ logger.info('Starting read_card')
847
817
 
848
818
  encoding = :waiting_for_selection
849
819
  loop do
@@ -867,12 +837,15 @@ module PWN
867
837
  end
868
838
  end
869
839
 
870
- wait_for_swipe(
840
+ track_data = wait_for_swipe(
871
841
  msr206_obj: msr206_obj,
872
842
  type: :arm_to_read,
873
843
  encoding: encoding
874
844
  )
845
+ logger.info("Read card successful: #{track_data.inspect}")
846
+ track_data
875
847
  rescue StandardError => e
848
+ logger.error("Error reading card: #{e.message}")
876
849
  raise e
877
850
  end
878
851
 
@@ -883,22 +856,14 @@ module PWN
883
856
 
884
857
  public_class_method def self.backup_card(opts = {})
885
858
  msr206_obj = opts[:msr206_obj]
886
- type = opts[:type].to_s.scrub.strip.chomp.to_sym
859
+ logger.info('Starting backup_card')
887
860
 
888
- # Read Card to Backup
889
- track_data = read_card(
890
- msr206_obj: msr206_obj
891
- )
861
+ track_data = read_card(msr206_obj: msr206_obj)
892
862
 
893
863
  file = ''
894
864
  backup_msg = ''
895
865
  loop do
896
- if backup_msg.empty?
897
- exec_resp = exec(
898
- msr206_obj: msr206_obj,
899
- cmd: :green_flash
900
- )
901
- end
866
+ exec(msr206_obj: msr206_obj, cmd: :green_flash) if backup_msg.empty?
902
867
 
903
868
  print 'Enter File Name to Save Backup: '
904
869
  file = gets.scrub.chomp.strip
@@ -907,34 +872,25 @@ module PWN
907
872
 
908
873
  backup_msg = "\n****** ERROR: Directory #{file_dir} for #{file} does not exist ******"
909
874
  puts backup_msg
910
- exec_resp = exec(
911
- msr206_obj: msr206_obj,
912
- cmd: :green_off
913
- )
914
- exec_resp = exec(
915
- msr206_obj: msr206_obj,
916
- cmd: :yellow_flash
917
- )
875
+ exec(msr206_obj: msr206_obj, cmd: :green_off)
876
+ exec(msr206_obj: msr206_obj, cmd: :yellow_flash)
918
877
  end
919
878
 
920
879
  File.write(file, "#{JSON.pretty_generate(track_data)}\n")
921
- exec_resp = exec(
922
- msr206_obj: msr206_obj,
923
- cmd: :yellow_off
924
- )
925
-
926
- puts 'complete.'
927
-
880
+ exec(msr206_obj: msr206_obj, cmd: :yellow_off)
881
+ logger.info("Backup saved to #{file}")
882
+ puts 'Backup complete.'
928
883
  track_data
929
884
  rescue StandardError => e
885
+ logger.error("Error backing up card: #{e.message}")
930
886
  raise e
931
887
  end
932
888
 
933
889
  # Supported Method Parameters::
934
890
  # PWN::Plugins::MSR206.write_card(
935
891
  # msr206_obj: 'required - msr206_obj returned from #connect method',
936
- # encoding: 'required - :iso || :alt_iso || :raw',
937
- # track_data: 'requred - track data to write (see #backup_card for structure)'
892
+ # encoding: 'required - :iso || :iso_alt || :raw',
893
+ # track_data: 'required - track data to write (see #backup_card for structure)'
938
894
  # )
939
895
 
940
896
  public_class_method def self.write_card(opts = {})
@@ -942,51 +898,56 @@ module PWN
942
898
  encoding = opts[:encoding].to_s.scrub.strip.chomp.to_sym
943
899
  track_data = opts[:track_data]
944
900
 
901
+ logger.info("Starting write_card with encoding: #{encoding}")
945
902
  puts 'IN ORDER TO GET BLANK TRACKS, A STRONG MAGNETIC FIELD MUST BE PRESENT TO FIRST WIPE THE CARD TARGETED FOR WRITING.'
946
- # puts 'Default Write Current:'
947
- # exec_resp = exec(
948
- # msr206_obj: msr206_obj,
949
- # cmd: :view_default_write_current
950
- # )
951
- # puts exec_resp.inspect
952
-
953
- # puts 'Temporary Write Current:'
954
- # exec_resp = exec(
955
- # msr206_obj: msr206_obj,
956
- # cmd: :view_temp_write_current
957
- # )
958
- # puts exec_resp.inspect
959
903
 
904
+ # Set track density
905
+ density = :waiting_for_selection
906
+ loop do
907
+ puts "\nTRACK DENSITY OPTIONS:"
908
+ puts '[(H)igh (210 BPI, Tracks 1 & 3)]'
909
+ puts '[(L)ow (75 BPI, Tracks 1 & 3)]'
910
+ puts '[(T2H)igh (210 BPI, Track 2)]'
911
+ puts '[(T2L)ow (75 BPI, Track 2)]'
912
+ print 'TRACK DENSITY >>> '
913
+ density_choice = gets.scrub.chomp.strip.upcase.to_sym
914
+
915
+ case density_choice
916
+ when :H
917
+ exec(msr206_obj: msr206_obj, cmd: :set_write_density_210_bpi_tracks13)
918
+ break
919
+ when :L
920
+ exec(msr206_obj: msr206_obj, cmd: :set_write_density_75_bpi_tracks13)
921
+ break
922
+ when :T2H
923
+ exec(msr206_obj: msr206_obj, cmd: :set_write_density_210_bpi_tracks2)
924
+ break
925
+ when :T2L
926
+ exec(msr206_obj: msr206_obj, cmd: :set_write_density_75_bpi_tracks2)
927
+ break
928
+ end
929
+ end
930
+
931
+ # Set coercivity
960
932
  coercivity = :waiting_for_selection
961
933
  loop do
962
934
  puts "\nCOERCIVITY OPTIONS:"
963
935
  puts '[(H)igh (Most Often Black Stripe)]'
964
- puts '[(L)ow (Most Often Brown Stripe)]'
936
+ puts '[(L)ow (Most Often Brown Stripe)]'
965
937
  print 'COERCIVITY LEVEL >>> '
966
938
  coercivity_choice = gets.scrub.chomp.strip.upcase.to_sym
967
939
 
968
- # Write Current Settings vs. Media Coercivties
969
- # Media Coercivity (Oersteds)|Write Current Setting*|Typical Usage
970
- # 300 |36 |Low coercivity
971
- # 600 | |
972
- # 1800 | |
973
- # 3600+ |255 |Typical high corcivity
974
-
975
940
  case coercivity_choice
976
941
  when :H
977
- coercivity = [0x32, 0x35, 0x35]
942
+ coercivity = [0x32, 0x35, 0x35] # 255 for high coercivity
978
943
  break
979
944
  when :L
980
- coercivity = [0x30, 0x33, 0x36]
945
+ coercivity = [0x30, 0x33, 0x36] # 36 for low coercivity
981
946
  break
982
947
  end
983
948
  end
984
949
 
985
- exec_resp = exec(
986
- msr206_obj: msr206_obj,
987
- cmd: :set_temp_write_current,
988
- params: coercivity
989
- )
950
+ exec(msr206_obj: msr206_obj, cmd: :set_temp_write_current, params: coercivity)
990
951
 
991
952
  track_data = wait_for_swipe(
992
953
  msr206_obj: msr206_obj,
@@ -995,29 +956,43 @@ module PWN
995
956
  track_data: track_data
996
957
  )
997
958
 
998
- exec_resp = PWN::Plugins::MSR206.exec(
999
- msr206_obj: msr206_obj,
1000
- cmd: :simulate_power_cycle_warm_reset
1001
- )
959
+ # Verify write
960
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :write_verify)
961
+ if exec_resp[:msg] == :ack_command_completed
962
+ puts 'Write verification successful.'
963
+ logger.info('Write verification successful')
964
+ else
965
+ puts "Write verification failed: #{exec_resp[:msg]}"
966
+ logger.error("Write verification failed: #{exec_resp[:msg]}")
967
+ end
1002
968
 
969
+ # Re-read card to confirm
970
+ read_data = read_card(msr206_obj: msr206_obj)
971
+ if read_data.map { |t| t[:decoded] } == track_data.map { |t| t[:decoded] }
972
+ puts 'Card data matches written data.'
973
+ logger.info('Card data matches written data')
974
+ else
975
+ puts 'ERROR: Written data does not match read data.'
976
+ logger.error('Written data does not match read data')
977
+ end
978
+
979
+ exec(msr206_obj: msr206_obj, cmd: :simulate_power_cycle_warm_reset)
1003
980
  track_data
1004
981
  rescue StandardError => e
982
+ logger.error("Error writing card: #{e.message}")
1005
983
  raise e
1006
984
  end
1007
985
 
1008
986
  # Supported Method Parameters::
1009
- # PWN::Plugins::MSR206.copy_card(
987
+ # PWN::Plugins::MSR206.clone_card(
1010
988
  # msr206_obj: 'required - msr206_obj returned from #connect method'
1011
989
  # )
1012
990
 
1013
- public_class_method def self.copy_card(opts = {})
991
+ public_class_method def self.clone_card(opts = {})
1014
992
  msr206_obj = opts[:msr206_obj]
993
+ logger.info('Starting clone_card')
1015
994
 
1016
- # Read Card to Backup
1017
- track_data = backup_card(
1018
- msr206_obj: msr206_obj
1019
- )
1020
-
995
+ track_data = backup_card(msr206_obj: msr206_obj)
1021
996
  encoding = track_data.first[:encoding] if track_data.length == 3
1022
997
  write_card(
1023
998
  msr206_obj: msr206_obj,
@@ -1025,6 +1000,7 @@ module PWN
1025
1000
  track_data: track_data
1026
1001
  )
1027
1002
  rescue StandardError => e
1003
+ logger.error("Error cloning card: #{e.message}")
1028
1004
  raise e
1029
1005
  end
1030
1006
 
@@ -1035,16 +1011,12 @@ module PWN
1035
1011
 
1036
1012
  public_class_method def self.load_card_from_file(opts = {})
1037
1013
  msr206_obj = opts[:msr206_obj]
1014
+ logger.info('Starting load_card_from_file')
1038
1015
 
1039
1016
  file = ''
1040
1017
  restore_msg = ''
1041
1018
  loop do
1042
- if restore_msg.empty?
1043
- exec_resp = exec(
1044
- msr206_obj: msr206_obj,
1045
- cmd: :green_flash
1046
- )
1047
- end
1019
+ exec(msr206_obj: msr206_obj, cmd: :green_flash) if restore_msg.empty?
1048
1020
 
1049
1021
  print 'Enter File Name to Restore to Card: '
1050
1022
  file = gets.scrub.chomp.strip
@@ -1052,90 +1024,147 @@ module PWN
1052
1024
 
1053
1025
  restore_msg = "\n****** ERROR: #{file} does not exist ******"
1054
1026
  puts restore_msg
1055
- exec_resp = exec(
1056
- msr206_obj: msr206_obj,
1057
- cmd: :green_off
1058
- )
1059
- exec_resp = exec(
1060
- msr206_obj: msr206_obj,
1061
- cmd: :yellow_flash
1062
- )
1027
+ exec(msr206_obj: msr206_obj, cmd: :green_off)
1028
+ exec(msr206_obj: msr206_obj, cmd: :yellow_flash)
1063
1029
  end
1064
1030
 
1065
- # Read Card from Backup
1066
- track_data = JSON.parse(
1067
- File.read(file),
1068
- symbolize_names: true
1069
- )
1031
+ track_data = JSON.parse(File.read(file), symbolize_names: true)
1032
+ exec(msr206_obj: msr206_obj, cmd: :yellow_off)
1070
1033
 
1071
- exec_resp = exec(
1072
- msr206_obj: msr206_obj,
1073
- cmd: :yellow_off
1074
- )
1075
-
1076
- encoding = track_data.first[:encoding] if track_data.length == 3
1034
+ encoding = track_data.first[:encoding] || :iso
1077
1035
  write_card(
1078
1036
  msr206_obj: msr206_obj,
1079
1037
  encoding: encoding,
1080
1038
  track_data: track_data
1081
1039
  )
1082
1040
  rescue StandardError => e
1041
+ logger.error("Error loading card from file: #{e.message}")
1083
1042
  raise e
1084
1043
  end
1085
1044
 
1086
1045
  # Supported Method Parameters::
1087
- # PWN::Plugins::MSR206.get_config(
1046
+ # PWN::Plugins::MSR206.update_card(
1088
1047
  # msr206_obj: 'required - msr206_obj returned from #connect method'
1089
1048
  # )
1090
1049
 
1091
- public_class_method def self.get_config(opts = {})
1050
+ public_class_method def self.update_card(opts = {})
1092
1051
  msr206_obj = opts[:msr206_obj]
1052
+ logger.info('Starting update_card')
1093
1053
 
1094
- # --------------------------------------------------
1095
- # Bit|Bit = 0 |Bit = 1
1096
- # --------------------------------------------------
1097
- # 0 |Track 1 Read not present |Track 1 Read present
1098
- # 1 |Track 2 Read not present |Track 2 Read present
1099
- # 2 |Track 3 Read not present |Track 3 Read present
1100
- # 3 |not used – should be 0 |not used
1101
- # 4 |Track 3 Write not present|Track 3 Write present
1102
- # 5 |Track 2 Write not present|Track 2 Write present
1103
- # 6 |Track 1 Write not present|Track 1 Write present
1104
- # 7 |parity bit** |parity bit**
1105
- exec_resp = PWN::Plugins::MSR206.exec(
1106
- msr206_obj: msr206_obj,
1107
- cmd: :configuration_request
1108
- )
1054
+ # Check for card presence
1055
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :card_edge_detect)
1056
+ unless exec_resp[:msg] == :card_edge_detected
1057
+ logger.error('Card not detected')
1058
+ raise 'Card not detected. Please ensure a card is inserted.'
1059
+ end
1109
1060
 
1110
- config_arr = exec_resp[:binary].first.reverse.chars
1111
- config_hash = {}
1112
- config_arr.each_with_index do |bit_str, i|
1113
- bit = bit_str.to_i
1114
- config_hash[:track1_read] = false if bit.zero? && i.zero?
1115
- config_hash[:track1_read] = true if bit == 1 && i.zero?
1061
+ # Read card to backup
1062
+ track_data = backup_card(msr206_obj: msr206_obj)
1063
+ unless track_data.is_a?(Array) && track_data.all? { |t| t.is_a?(Hash) && t.key?(:decoded) }
1064
+ logger.error('Invalid track data structure')
1065
+ raise 'Invalid track data structure'
1066
+ end
1116
1067
 
1117
- config_hash[:track2_read] = false if bit.zero? && i == 1
1118
- config_hash[:track2_read] = true if bit == 1 && i == 1
1068
+ # Update each track's decoded value
1069
+ track_data.each_with_index do |track, index|
1070
+ decoded = track[:decoded]
1071
+ puts "\nTrack #{index + 1} Current Value: #{decoded}"
1072
+ print 'Enter Updated Value (press Enter to keep original): '
1073
+ updated_value = gets.scrub.chomp.strip
1074
+
1075
+ # Retain original value if empty
1076
+ if updated_value.empty?
1077
+ puts "Keeping original value: #{decoded}"
1078
+ logger.info("Track #{index + 1}: Keeping original value: #{decoded}")
1079
+ updated_value = decoded
1080
+ else
1081
+ # Validate input for ISO encoding
1082
+ if track[:encoding] == :iso
1083
+ max_length = case index
1084
+ when 0 then 79 # Track 1
1085
+ when 1 then 40 # Track 2
1086
+ when 2 then 107 # Track 3
1087
+ end
1088
+ unless updated_value.length <= max_length
1089
+ logger.error("Track #{index + 1}: Input too long (max #{max_length} characters)")
1090
+ raise "Track #{index + 1}: Input too long (max #{max_length} characters)"
1091
+ end
1092
+ unless updated_value.match?(/\A[ -~]*\z/) # ASCII printable characters only
1093
+ logger.error("Track #{index + 1}: Invalid characters for ISO encoding")
1094
+ raise "Track #{index + 1}: Invalid characters for ISO encoding"
1095
+ end
1096
+ end
1097
+ logger.info("Track #{index + 1}: Updated value: #{updated_value}")
1098
+ end
1119
1099
 
1120
- config_hash[:track3_read] = false if bit.zero? && i == 2
1121
- config_hash[:track3_read] = true if bit == 1 && i == 2
1100
+ track[:decoded] = updated_value
1101
+ end
1122
1102
 
1123
- config_hash[:not_used] if i == 3
1103
+ # Confirm changes
1104
+ puts "\nUpdated Track Data:"
1105
+ track_data.each_with_index { |t, i| puts "Track #{i + 1}: #{t[:decoded]}" }
1106
+ print 'Confirm writing these changes to the card? [y/N]: '
1107
+ unless gets.chomp.strip.upcase == 'Y'
1108
+ logger.info('Update cancelled by user')
1109
+ puts 'Update cancelled.'
1110
+ return track_data
1111
+ end
1124
1112
 
1125
- config_hash[:track1_write] = false if bit.zero? && i == 4
1126
- config_hash[:track1_write] = true if bit == 1 && i == 4
1113
+ # Write updated data
1114
+ encoding = track_data.first[:encoding] || :iso
1115
+ track_data = write_card(
1116
+ msr206_obj: msr206_obj,
1117
+ encoding: encoding,
1118
+ track_data: track_data
1119
+ )
1127
1120
 
1128
- config_hash[:track2_write] = false if bit.zero? && i == 5
1129
- config_hash[:track2_write] = true if bit == 1 && i == 5
1121
+ logger.info("Update card successful: #{track_data.inspect}")
1122
+ puts 'Card updated successfully.'
1123
+ track_data
1124
+ rescue StandardError => e
1125
+ logger.error("Error updating card: #{e.message}")
1126
+ puts "ERROR: Failed to update card - #{e.message}"
1127
+ raise e
1128
+ end
1130
1129
 
1131
- config_hash[:track3_write] = false if bit.zero? && i == 6
1132
- config_hash[:track3_write] = true if bit == 1 && i == 6
1130
+ # Supported Method Parameters::
1131
+ # PWN::Plugins::MSR206.get_config(
1132
+ # msr206_obj: 'required - msr206_obj returned from #connect method'
1133
+ # )
1134
+
1135
+ public_class_method def self.get_config(opts = {})
1136
+ msr206_obj = opts[:msr206_obj]
1137
+ logger.info('Retrieving configuration')
1138
+
1139
+ exec_resp = exec(msr206_obj: msr206_obj, cmd: :configuration_request)
1140
+ config_arr = exec_resp[:binary].first.reverse.chars
1141
+ config_hash = {
1142
+ track1_read: false,
1143
+ track2_read: false,
1144
+ track3_read: false,
1145
+ track1_write: false,
1146
+ track2_write: false,
1147
+ track3_write: false,
1148
+ not_used: false,
1149
+ parity: false
1150
+ }
1133
1151
 
1134
- config_hash[:parity] = true if bit == 1 && i == 7
1152
+ config_arr.each_with_index do |bit_str, i|
1153
+ bit = bit_str.to_i
1154
+ config_hash[:track1_read] = true if i.zero? && bit == 1
1155
+ config_hash[:track2_read] = true if i == 1 && bit == 1
1156
+ config_hash[:track3_read] = true if i == 2 && bit == 1
1157
+ config_hash[:not_used] = true if i == 3 && bit == 1
1158
+ config_hash[:track3_write] = true if i == 4 && bit == 1
1159
+ config_hash[:track2_write] = true if i == 5 && bit == 1
1160
+ config_hash[:track1_write] = true if i == 6 && bit == 1
1161
+ config_hash[:parity] = true if i == 7 && bit == 1
1135
1162
  end
1136
1163
 
1164
+ logger.info("Configuration: #{config_hash.inspect}")
1137
1165
  config_hash
1138
1166
  rescue StandardError => e
1167
+ logger.error("Error getting configuration: #{e.message}")
1139
1168
  raise e
1140
1169
  end
1141
1170
 
@@ -1145,10 +1174,10 @@ module PWN
1145
1174
  # )
1146
1175
 
1147
1176
  public_class_method def self.disconnect(opts = {})
1148
- PWN::Plugins::Serial.disconnect(
1149
- serial_obj: opts[:msr206_obj]
1150
- )
1177
+ logger.info('Disconnecting from device')
1178
+ PWN::Plugins::Serial.disconnect(serial_obj: opts[:msr206_obj])
1151
1179
  rescue StandardError => e
1180
+ logger.error("Error disconnecting: #{e.message}")
1152
1181
  raise e
1153
1182
  end
1154
1183
 
@@ -1166,10 +1195,16 @@ module PWN
1166
1195
  puts "USAGE:
1167
1196
  msr206_obj = #{self}.connect(
1168
1197
  block_dev: 'optional serial block device path (defaults to /dev/ttyUSB0)',
1169
- baud: 'optional (defaults to 9600)',
1198
+ baud: 'optional (defaults to 19200)',
1170
1199
  data_bits: 'optional (defaults to 8)',
1171
1200
  stop_bits: 'optional (defaults to 1)',
1172
- parity: 'optional - :even||:odd|:none (defaults to :none)'
1201
+ parity: 'optional - :even||:odd|:none (defaults to :none)',
1202
+ flow_control: 'optional - :none||:hard||:soft (defaults to :soft)'
1203
+ )
1204
+
1205
+ #{self}.set_protocol(
1206
+ msr206_obj: 'required - msr206_obj returned from #connect method',
1207
+ protocol: 'optional - :usi0 or :usi1 (defaults to :usi0)'
1173
1208
  )
1174
1209
 
1175
1210
  cmds = #{self}.list_cmds
@@ -1177,7 +1212,37 @@ module PWN
1177
1212
  parsed_cmd_resp_arr = #{self}.exec(
1178
1213
  msr206_obj: 'required msr206_obj returned from #connect method',
1179
1214
  cmd: 'required - cmd returned from #list_cmds method',
1180
- params: 'optional - parameters for specific command returned from #list_params method'
1215
+ params: 'optional - parameters for specific command'
1216
+ )
1217
+
1218
+ track_data = #{self}.read_card(
1219
+ msr206_obj: 'required msr206_obj returned from #connect method'
1220
+ )
1221
+
1222
+ track_data = #{self}.backup_card(
1223
+ msr206_obj: 'required msr206_obj returned from #connect method'
1224
+ )
1225
+
1226
+ track_data = #{self}.write_card(
1227
+ msr206_obj: 'required msr206_obj returned from #connect method',
1228
+ encoding: 'required - :iso || :iso_alt || :raw',
1229
+ track_data: 'required - track data to write'
1230
+ )
1231
+
1232
+ track_data = #{self}.clone_card(
1233
+ msr206_obj: 'required msr206_obj returned from #connect method'
1234
+ )
1235
+
1236
+ track_data = #{self}.load_card_from_file(
1237
+ msr206_obj: 'required msr206_obj returned from #connect method'
1238
+ )
1239
+
1240
+ track_data = #{self}.update_card(
1241
+ msr206_obj: 'required msr206_obj returned from #connect method'
1242
+ )
1243
+
1244
+ config = #{self}.get_config(
1245
+ msr206_obj: 'required msr206_obj returned from #connect method'
1181
1246
  )
1182
1247
 
1183
1248
  #{self}.disconnect(