Module: PWN::SDR::Decoder::DECT
- Defined in:
- lib/pwn/sdr/decoder/dect.rb
Overview
DECT P00 control and P32 B-field descrambling, ETSI EN 300 175-3 sections 6.2/7.1. https://www.etsi.org/deliver/etsi_EN/300100_300199/30017503/02.08.01_60/en_30017503v020801p.pdf
1.152 Mbit/s GFSK, 24-slot / 10 ms TDMA. Continuous Ruby FM/NRZ symbol recovery → hunt 32-bit S-field (16-bit preamble + 16-bit sync 0xE98A FP / 0x1675 PP) → A-field (64 bits: 8-bit header + 40-bit tail + 16-bit R-CRC) → RFPI extraction on Nt/Qt tails. Emits role:, slot_est:, crc_ok:.
Defined Under Namespace
Classes: DemodIQ
Constant Summary collapse
- SYNC_FP =
0xAAAAE98A
- SYNC_PP =
0x55551675- BAUD =
1_152_000- RCRC_POLY =
R-CRC-16 poly x^16+x^10+x^8+x^7+x^3+1 = 0x0589, init 0x0000.
0x0589- A_TA =
{ 0 => 'Ct', 1 => 'Ct', 2 => 'Nt', 3 => 'Nt', 4 => 'Qt', 5 => 'combined', 6 => 'Mt', 7 => 'Pt' }.freeze
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.decode(opts = {}) ⇒ Object
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).
-
.detect(opts = {}) ⇒ Object
Energy observations only; never substituted for protocol decoding.
- .help ⇒ Object
-
.parse_b_field(opts = {}) ⇒ Object
EN 300 175-3 6.2.4 and 6.2.1.3: externally supplied frame/connection context.
- .parse_line(opts = {}) ⇒ Object
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
241 242 243 |
# File 'lib/pwn/sdr/decoder/dect.rb', line 241 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.decode(opts = {}) ⇒ Object
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).
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/pwn/sdr/decoder/dect.rb', line 198 public_class_method def self.decode(opts = {}) raise ArgumentError, 'supported packets are :p00 and :p32' unless i[p00 p32].include?(opts.fetch(:packet, :p00)) freq_obj = opts[:freq_obj] hz = PWN::SDR.hz_to_i(freq: freq_obj[:freq]) # EU: carrier 0 = 1897.344 MHz, step 1.728 MHz down; US 1.9296 GHz. carrier = ((1_897_344_000 - hz) / 1_728_000.0).round rate = (opts[:sample_rate] || freq_obj[:iq_rate] || 2_304_000).to_i PWN::SDR::Decoder::Base.run_iq( **opts, freq_obj: freq_obj, protocol: 'DECT', sample_rate: rate, source: opts[:source], file: opts[:file], demod: DemodIQ.new(rate: rate, carrier: carrier, packet: opts.fetch(:packet, :p00), encrypted: opts[:encrypted], frame_number: opts[:frame_number], b_format: opts.fetch(:b_format, :unprotected)), fallback: :raise, note: '1.152 Mbit/s GFSK — I/Q→gmskdem→S-field 0xE98A→A-field/RFPI/R-CRC.', describe: proc { |b| { modulation: 'GFSK', tdma_slots: (b[:duration_ms] / 0.417).round } } ) end |
.detect(opts = {}) ⇒ Object
Energy observations only; never substituted for protocol decoding.
222 223 224 225 |
# File 'lib/pwn/sdr/decoder/dect.rb', line 222 public_class_method def self.detect(opts = {}) Base.run_detector(opts.merge(protocol: 'DECT', note: 'Energy detector only; use .decode for supported protocol frames.')) end |
.help ⇒ Object
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 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/dect.rb', line 245 public_class_method def self.help puts "USAGE: # Supported: packet: :p00 control (default), or :p32 full-slot framing. # R-CRC protects A-field; X-CRC covers ONLY 80 selected scrambled B bits. # encrypted: true/false/nil supplies external connection context; nil is unknown. # P32 frame_number: 0..15 enables descrambling. A callable supplies each packet's frame. # A constant frame number applies to EVERY packet; no automatic multiframe tracking. # b_format: :unprotected, :multisubfield (4x64+16 CRC), :singlesubfield (304+16 CRC). # Format/encryption are caller-owned connection context, not inferred from BA alone. # No DSC decryption, encoded IPX/FEC, E/U channel demux, DLC reassembly or speech codec. # Descramble a P32 B-field and verify configured protected subfields. #{self}.parse_b_field(bits: 'required - 320 bits', frame_number: 'required - TDMA frame 0..15', encrypted: false, b_format: :multisubfield) # Role assumes the observed sync polarity; spectral inversion can swap FP/PP. #{self}.detect(freq_obj: 'required', on_frame: 'optional callback') # 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', encrypted: 'optional - true or false for known connection encryption, nil if unknown', frame_number: 'optional - constant TDMA frame 0..15 or per-packet callable', 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_b_field(opts = {}) ⇒ Object
EN 300 175-3 6.2.4 and 6.2.1.3: externally supplied frame/connection context.
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/pwn/sdr/decoder/dect.rb', line 148 public_class_method def self.parse_b_field(opts = {}) bits = opts[:bits] frame = opts[:frame_number] format = opts.fetch(:b_format, :unprotected) raise ArgumentError, 'B-field must contain 320 bits' unless bits.is_a?(Array) && bits.length == 320 && bits.all? { |b| [0, 1].include?(b) } raise ArgumentError, 'frame_number must be 0..15' unless frame.is_a?(Integer) && (0..15).cover?(frame) raise ArgumentError, 'unsupported B-field format' unless i[unprotected multisubfield singlesubfield].include?(format) register = 24 | (frame & 7) invert = 1 clear = bits.map do |bit| output = bit ^ (register >> 4) ^ invert invert ^= 1 if register == 31 feedback = ((register >> 1) ^ (register >> 4)) & 1 register = ((register << 1) & 31) | feedback output end hex = ->(data) { DSP.bytes_from_bits(bits: data).pack('C*').unpack1('H*').upcase } result = { frame_number: frame, b_format: format, payload_hex: nil, b_crc_ok: nil, descrambled_payload_hex: hex.call(clear), payload_state: 'encryption-unknown' } if opts[:encrypted] != false return result.merge(payload_state: opts[:encrypted] ? 'encrypted' : 'encryption-unknown', ciphertext_hex: hex.call(clear)) end subfields = if format == :unprotected [clear] else clear.each_slice(format == :multisubfield ? 80 : 320).to_a end if format != :unprotected return nil unless subfields.all? do |field| crc = DSP.crc16(bytes: DSP.bytes_from_bits(bits: field[0...-16]), poly: RCRC_POLY, init: 0) ^ 1 crc == DSP.bits_to_int(bits: field[-16, 16]) end subfields = subfields.map { |field| field[0...-16] } end result.merge(payload_state: 'clear', payload_hex: hex.call(subfields.flatten), subfields: subfields.map { |field| { payload_hex: hex.call(field) } }, b_crc_ok: format == :unprotected ? nil : true) end |
.parse_line(opts = {}) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/pwn/sdr/decoder/dect.rb', line 227 public_class_method def self.parse_line(opts = {}) line = opts[:line].to_s out = { protocol: 'DECT' } out[:rfpi] = ::Regexp.last_match(1).delete(' ') if line =~ /RFPI[:=]?\s*((?:[0-9A-Fa-f]{2}\s*){5})/ out[:slot] = ::Regexp.last_match(1) if line =~ /slot\s*(\d+)/i out[:carrier] = ::Regexp.last_match(1) if line =~ /carrier\s*(\d+)/i out[:rssi] = ::Regexp.last_match(1) if line =~ /RSSI[:=]?\s*(-?\d+)/i out[:role] = ::Regexp.last_match(1) if line =~ /\b(FP|PP)\b/ out[:summary] = "DECT RFPI=#{out[:rfpi]} slot=#{out[:slot]} carrier=#{out[:carrier]}" out.compact end |