Module: PWN::SDR::Decoder::RFID

Defined in:
lib/pwn/sdr/decoder/rfid.rb

Overview

Protocol frames via .decode; energy observations only via .detect.

Defined Under Namespace

Classes: DetectorIQ, EM4100IQ

Constant Summary collapse

SUPPORTED_MODES =

Realtime options forwarded to Base: on_frame (Hash callback), output (writable IO), interactive (default true), duration (seconds), stop (callable), queue_size (bounded chunks), log_file (path or false).

i[em4100 fdxb_pm3].freeze
DemodIQ =
EM4100IQ

Class Method Summary collapse

Class Method Details

.authorsObject

Author(s)

0day Inc. [email protected]



302
303
304
# File 'lib/pwn/sdr/decoder/rfid.rb', line 302

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <[email protected]>\n"
end

.decode(opts = {}) ⇒ Object

Never silently downgrade decode to energy detection.

Raises:

  • (ArgumentError)


166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/pwn/sdr/decoder/rfid.rb', line 166

public_class_method def self.decode(opts = {})
  mode = opts.fetch(:mode, :em4100).to_s.to_sym
  raise ArgumentError, "unsupported RFID mode #{mode}; supported: #{SUPPORTED_MODES.join(', ')}" unless SUPPORTED_MODES.include?(mode)

  return fdxb_replay(opts) if mode == :fdxb_pm3

  freq_obj = opts[:freq_obj] || {}
  rate = opts[:sample_rate] || freq_obj[:iq_rate] || 250_000
  demod = EM4100IQ.new(rate: rate, carrier_hz: opts.fetch(:carrier_hz, 125_000),
                       clocks_per_bit: opts.fetch(:clocks_per_bit, 64), threshold: opts.fetch(:threshold, 0.5))
  PWN::SDR::Decoder::Base.run_iq(opts.merge(freq_obj: freq_obj, protocol: 'RFID',
                                            sample_rate: rate, demod: demod, fallback: :raise))
end

.detect(opts = {}) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/pwn/sdr/decoder/rfid.rb', line 260

public_class_method def self.detect(opts = {})
  freq_obj = opts[:freq_obj]
  hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq])
  band = if hz < 1_000_000 then 'LF'
         elsif hz.between?(13_000_000, 14_000_000) then 'HF'
         else 'UHF'
         end

  rate  = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_000_000).to_i
  proto = "RFID-#{band}"
  demod = DetectorIQ.new(
    rate: rate, protocol: proto, modulation: 'ASK/load-mod',
    extra: { threshold: 6.0 }
  )
  PWN::SDR::Decoder::Base.run_iq(
    **opts,
    freq_obj: freq_obj,
    protocol: proto,
    sample_rate: rate,
    source: opts[:source],
    file: opts[:file],
    demod: demod,
    threshold: 6.0,
    note: 'True-air I/Q path reports reader-carrier and tag-backscatter bursts by band.',
    describe: proc { |b| { band: b[:band], modulation: 'ASK/load-mod', classification: b[:classification] } }
  )
end

.helpObject



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/pwn/sdr/decoder/rfid.rb', line 306

public_class_method def self.help
  puts "USAGE:
    # Default :em4100 is ASK Manchester, not PSK/biphase/HF/UHF.
    # :fdxb_pm3 uses installed proxmark3 offline demodulation of ISO11784/11785 FDX-B.
    # file: explicit .pm3 signed integer AMPLITUDE trace, not SDR IQ; no hardware access.
    # Valid CRC16 required. Returns national/country ID, animal and data-block flags.
    # executable: 'proxmark3'; duration: 30s deadline; stop: callable; output/on_frame/log_file.
    # Missing executable raises IOError with install/path guidance; already-stopped replay never spawns.
    # PM3 mode is finite offline replay; no ISO14443/ISO15693/EPC Gen2 coverage.
    # carrier_hz: 125000 default; clocks_per_bit: 64 default, or 32/16.
    # threshold: 0.5 default, normalized envelope amplitude; tune to the received levels.
    # sample_rate must provide >=4 samples/half-bit; default 250000.
    # Recovers polarity/timing at transitions; validates all row/column parity and stop.
    # decode never falls back to a detector; unsupported mode raises ArgumentError.
    # #{self}.detect uses the same source/lifecycle options for energy-only observations.
    # Validate a complete frame and return decoded fields or nil.
    #{self}.parse_frame(bits: 'required - Array of 64 binary digits in transmitted order')
    # Run decode and return its result
    #{self}.decode(
      freq_obj: 'required - freq_obj returned from PWN::SDR::GQRX.init_freq',
      on_frame: 'optional - callback receiving each emitted Hash',
      output: 'optional - writable IO (default stdout)',
      interactive: 'optional - false disables ENTER input',
      duration: 'optional - finite seconds to run',
      stop: 'optional - callable returning true to stop',
      queue_size: 'optional - bounded pending chunks (default 8)',
      log_file: 'optional - JSONL path or false to disable logging',
      sample_rate: 'optional - sample rate value consumed by #decode',
      source: 'optional - source value consumed by #decode',
      file: 'optional - filesystem path'
    )

    # Run parse line and return its result
    #{self}.parse_line(
      line: 'optional - line value consumed by #parse_line'
    )

    # Print the AUTHOR(S) string for this module.
    #{self}.authors
  "
  constants.sort
end

.parse_frame(opts = {}) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/pwn/sdr/decoder/rfid.rb', line 139

public_class_method def self.parse_frame(opts = {})
  bits = opts[:bits]
  return nil unless bits.is_a?(Array) && bits.length == 64 && bits.all? { |b| [0, 1].include?(b) }
  return nil unless bits.first(9) == [1] * 9 && bits.last.zero?

  rows = bits[9, 50].each_slice(5).to_a
  return nil unless rows.all? { |row| row.sum.even? }
  return nil unless 4.times.all? { |column| (rows.sum { |row| row[column] } + bits[59 + column]).even? }

  uid = rows.map { |row| row.first(4).join.to_i(2).to_s(16) }.join.upcase
  { protocol: 'RFID', mode: :em4100, source: 'iq', decoded: true,
    integrity: 'row-and-column-even-parity', uid: uid, version: uid[0, 2].to_i(16),
    identifier: uid[2, 8], summary: "EM4100 UID=#{uid}" }
end

.parse_line(opts = {}) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/pwn/sdr/decoder/rfid.rb', line 288

public_class_method def self.parse_line(opts = {})
  line = opts[:line].to_s
  out  = { protocol: 'RFID' }
  out[:uid]  = ::Regexp.last_match(1).delete(' ') if line =~ /UID[:=]?\s*((?:[0-9A-Fa-f]{2}\s*){4,10})/
  out[:epc]  = ::Regexp.last_match(1) if line =~ /EPC[:=]?\s*([0-9A-Fa-f]+)/
  out[:atqa] = ::Regexp.last_match(1) if line =~ /ATQA[:=]?\s*([0-9A-Fa-f ]+)/
  out[:sak]  = ::Regexp.last_match(1) if line =~ /SAK[:=]?\s*([0-9A-Fa-f]+)/
  out[:tag]  = ::Regexp.last_match(1) if line =~ /(EM4\w+|HID\w*|Mifare\w*|NTAG\w*|ISO\s?\d+)/i
  out[:summary] = "RFID #{out[:tag]} UID=#{out[:uid] || out[:epc]}".squeeze(' ')
  out.compact
end