pwn 0.5.621 → 0.5.626

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.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/README.md +7 -1
  5. data/bin/pwn_gqrx_scanner +78 -9
  6. data/documentation/Agent-Tool-Registry.md +3 -3
  7. data/documentation/Configuration.md +301 -47
  8. data/documentation/Cron.md +5 -3
  9. data/documentation/Diagrams.md +1 -1
  10. data/documentation/Extrospection.md +472 -26
  11. data/documentation/FFI.md +72 -8
  12. data/documentation/Home.md +4 -4
  13. data/documentation/How-PWN-Works.md +7 -3
  14. data/documentation/Mistakes.md +9 -0
  15. data/documentation/SDR.md +83 -10
  16. data/documentation/Skills-Memory-Learning.md +101 -11
  17. data/documentation/diagrams/agent-tool-registry.svg +56 -55
  18. data/documentation/diagrams/ai-integration-tool-calling.svg +24 -24
  19. data/documentation/diagrams/aws-cloud-security.svg +25 -25
  20. data/documentation/diagrams/burp-vs-zap-preference.svg +12 -12
  21. data/documentation/diagrams/code-scanning-sast.svg +25 -25
  22. data/documentation/diagrams/cron-scheduling.svg +23 -23
  23. data/documentation/diagrams/dot/agent-tool-registry.dot +2 -2
  24. data/documentation/diagrams/dot/extrospection-world-awareness.dot +33 -6
  25. data/documentation/diagrams/dot/hardware-hacking.dot +3 -1
  26. data/documentation/diagrams/dot/memory-skills-detailed.dot +4 -2
  27. data/documentation/diagrams/dot/mistakes-negative-feedback.dot +4 -2
  28. data/documentation/diagrams/dot/overall-pwn-architecture.dot +3 -3
  29. data/documentation/diagrams/dot/persistence-filesystem.dot +3 -3
  30. data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +10 -4
  31. data/documentation/diagrams/dot/sdr-radio-flow.dot +39 -16
  32. data/documentation/diagrams/driver-framework.svg +13 -13
  33. data/documentation/diagrams/extrospection-world-awareness.svg +383 -177
  34. data/documentation/diagrams/fuzzing-workflow.svg +24 -24
  35. data/documentation/diagrams/hardware-hacking.svg +84 -69
  36. data/documentation/diagrams/history-to-drivers.svg +18 -18
  37. data/documentation/diagrams/memory-skills-detailed.svg +137 -120
  38. data/documentation/diagrams/mistakes-negative-feedback.svg +152 -137
  39. data/documentation/diagrams/network-infra-testing.svg +27 -27
  40. data/documentation/diagrams/overall-pwn-architecture.svg +97 -97
  41. data/documentation/diagrams/penetration-testing-workflow.svg +30 -30
  42. data/documentation/diagrams/persistence-filesystem.svg +100 -94
  43. data/documentation/diagrams/plugin-ecosystem.svg +35 -35
  44. data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +254 -207
  45. data/documentation/diagrams/pwn-repl-prototyping.svg +20 -20
  46. data/documentation/diagrams/reporting-pipeline.svg +18 -18
  47. data/documentation/diagrams/reverse-engineering-flow.svg +21 -21
  48. data/documentation/diagrams/sdr-radio-flow.svg +223 -91
  49. data/documentation/diagrams/sessions-cron-automation.svg +18 -18
  50. data/documentation/diagrams/swarm-multi-agent.svg +39 -39
  51. data/documentation/diagrams/web-application-testing.svg +26 -26
  52. data/documentation/diagrams/zero-day-research-flow.svg +25 -25
  53. data/documentation/pwn-ai-Agent.md +16 -10
  54. data/documentation/pwn_silent_help_learn_demo.gif +0 -0
  55. data/lib/pwn/ai/agent/assembly.rb +1 -1
  56. data/lib/pwn/ai/agent/btc.rb +1 -1
  57. data/lib/pwn/ai/agent/burp_suite.rb +1 -1
  58. data/lib/pwn/ai/agent/extrospection.rb +2562 -57
  59. data/lib/pwn/ai/agent/gqrx.rb +2 -2
  60. data/lib/pwn/ai/agent/hacker_one.rb +1 -1
  61. data/lib/pwn/ai/agent/learning.rb +16 -16
  62. data/lib/pwn/ai/agent/loop.rb +1 -1
  63. data/lib/pwn/ai/agent/{introspection.rb → reflect.rb} +12 -12
  64. data/lib/pwn/ai/agent/sast.rb +1 -1
  65. data/lib/pwn/ai/agent/tools/extrospection.rb +311 -8
  66. data/lib/pwn/ai/agent/tools/learning.rb +7 -7
  67. data/lib/pwn/ai/agent/transparent_browser.rb +1 -1
  68. data/lib/pwn/ai/agent/vuln_gen.rb +2 -2
  69. data/lib/pwn/ai/agent.rb +1 -1
  70. data/lib/pwn/ai/open_ai.rb +10 -6
  71. data/lib/pwn/config.rb +5 -4
  72. data/lib/pwn/ffi/adalm_pluto.rb +498 -0
  73. data/lib/pwn/ffi/fftw.rb +192 -0
  74. data/lib/pwn/ffi/hack_rf.rb +193 -0
  75. data/lib/pwn/ffi/liquid.rb +244 -0
  76. data/lib/pwn/ffi/rtl_sdr.rb +210 -0
  77. data/lib/pwn/ffi/soapy_sdr.rb +177 -0
  78. data/lib/pwn/ffi/stdio.rb +9 -1
  79. data/lib/pwn/ffi/volk.rb +241 -0
  80. data/lib/pwn/ffi.rb +45 -2
  81. data/lib/pwn/plugins/burp_suite.rb +2 -2
  82. data/lib/pwn/sast/pom_version.rb +2 -2
  83. data/lib/pwn/sast/test_case_engine.rb +1 -1
  84. data/lib/pwn/sdr/decoder/adsb.rb +201 -12
  85. data/lib/pwn/sdr/decoder/apt.rb +19 -5
  86. data/lib/pwn/sdr/decoder/base.rb +314 -1
  87. data/lib/pwn/sdr/decoder/bluetooth.rb +91 -18
  88. data/lib/pwn/sdr/decoder/dect.rb +81 -23
  89. data/lib/pwn/sdr/decoder/dsp.rb +229 -15
  90. data/lib/pwn/sdr/decoder/flex.rb +23 -6
  91. data/lib/pwn/sdr/decoder/gps.rb +80 -18
  92. data/lib/pwn/sdr/decoder/gsm.rb +83 -28
  93. data/lib/pwn/sdr/decoder/iridium.rb +80 -18
  94. data/lib/pwn/sdr/decoder/lora.rb +84 -28
  95. data/lib/pwn/sdr/decoder/lte.rb +80 -18
  96. data/lib/pwn/sdr/decoder/morse.rb +23 -6
  97. data/lib/pwn/sdr/decoder/p25.rb +90 -31
  98. data/lib/pwn/sdr/decoder/pager.rb +23 -6
  99. data/lib/pwn/sdr/decoder/pocsag.rb +23 -6
  100. data/lib/pwn/sdr/decoder/rds.rb +219 -52
  101. data/lib/pwn/sdr/decoder/rfid.rb +81 -27
  102. data/lib/pwn/sdr/decoder/rtl433.rb +85 -27
  103. data/lib/pwn/sdr/decoder/rtty.rb +23 -6
  104. data/lib/pwn/sdr/decoder/wifi.rb +86 -32
  105. data/lib/pwn/sdr/decoder/zigbee.rb +88 -35
  106. data/lib/pwn/sdr/decoder.rb +6 -5
  107. data/lib/pwn/sdr/frequency_allocation.rb +124 -81
  108. data/lib/pwn/sdr/gqrx.rb +1513 -165
  109. data/lib/pwn/version.rb +1 -1
  110. data/spec/lib/pwn/ai/agent/extrospection_spec.rb +96 -0
  111. data/spec/lib/pwn/ai/agent/{introspection_spec.rb → reflect_spec.rb} +3 -3
  112. data/spec/lib/pwn/ai/agent/tools/extrospection_spec.rb +8 -24
  113. data/spec/lib/pwn/ffi/adalm_pluto_spec.rb +37 -0
  114. data/spec/lib/pwn/ffi/fftw_spec.rb +26 -0
  115. data/spec/lib/pwn/ffi/hack_rf_spec.rb +25 -0
  116. data/spec/lib/pwn/ffi/liquid_spec.rb +42 -0
  117. data/spec/lib/pwn/ffi/rtl_sdr_spec.rb +24 -0
  118. data/spec/lib/pwn/ffi/soapy_sdr_spec.rb +25 -0
  119. data/spec/lib/pwn/ffi/volk_spec.rb +36 -0
  120. data/spec/lib/pwn/sdr/decoder/base_iq_spec.rb +65 -0
  121. data/spec/lib/pwn/sdr/decoder/dsp_spec.rb +50 -4
  122. data/spec/lib/pwn/sdr/decoder/rds_spec.rb +45 -4
  123. data/third_party/pwn_rdoc.jsonl +279 -30
  124. metadata +21 -5
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
6
+
7
+ module PWN
8
+ module FFI
9
+ # Thin single-precision FFTW3 binding (`libfftw3f`).
10
+ #
11
+ # Used by PWN::SDR::* spectrum work (GQRX FFT snapshots, wideband energy
12
+ # detectors) when MHz-rate FFTs outgrow pure-Ruby DFT. Missing library
13
+ # degrades cleanly via `.available?` — callers keep pure-Ruby fallbacks.
14
+ #
15
+ # No compile step at gem install; no shells. Plans are built with
16
+ # FFTW_ESTIMATE so first-call latency stays acceptable for REPL use.
17
+ module FFTW
18
+ extend PubFFI::Library
19
+
20
+ FFTW_ESTIMATE = (1 << 6)
21
+ FFTW_MEASURE = 0
22
+ FFTW_FORWARD = -1
23
+ FFTW_BACKWARD = 1
24
+
25
+ @load_error = nil
26
+ begin
27
+ ffi_lib %w[fftw3f libfftw3f.so.3 libfftw3f.so]
28
+ rescue LoadError => e
29
+ @load_error = e
30
+ end
31
+
32
+ class << self
33
+ attr_reader :load_error
34
+ end
35
+
36
+ unless @load_error
37
+ # fftwf_complex is float[2] (re, im) under the C ABI we target.
38
+ attach_function :fftwf_malloc, [:size_t], :pointer
39
+ attach_function :fftwf_free, [:pointer], :void
40
+ attach_function :fftwf_destroy_plan, [:pointer], :void
41
+ attach_function :fftwf_cleanup, [], :void
42
+ attach_function :fftwf_execute, [:pointer], :void
43
+
44
+ # Real → complex 1-D (out has n/2+1 complex bins = 2*(n/2+1) floats)
45
+ attach_function :fftwf_plan_dft_r2c_1d,
46
+ %i[int pointer pointer uint],
47
+ :pointer
48
+
49
+ # Complex → real 1-D
50
+ attach_function :fftwf_plan_dft_c2r_1d,
51
+ %i[int pointer pointer uint],
52
+ :pointer
53
+
54
+ # Complex ↔ complex 1-D
55
+ attach_function :fftwf_plan_dft_1d,
56
+ %i[int pointer pointer int uint],
57
+ :pointer
58
+ end
59
+
60
+ # Supported Method Parameters::
61
+ # PWN::FFI::FFTW.available?
62
+
63
+ public_class_method def self.available?
64
+ !@load_error && respond_to?(:fftwf_malloc, true)
65
+ rescue StandardError
66
+ false
67
+ end
68
+
69
+ # Supported Method Parameters::
70
+ # spectrum = PWN::FFI::FFTW.rfft(
71
+ # samples: 'required - Array<Float> real input (length = n)',
72
+ # n: 'optional - FFT size (default samples.length; zero-pads/truncates)'
73
+ # )
74
+ # Returns Array of [re, im] pairs, length n/2+1 (DC .. Nyquist).
75
+
76
+ public_class_method def self.rfft(opts = {})
77
+ raise 'ERROR: libfftw3f not available' unless available?
78
+
79
+ samples = opts[:samples]
80
+ n = (opts[:n] || samples.length).to_i
81
+ raise 'ERROR: n must be >= 1' if n < 1
82
+
83
+ in_ptr = fftwf_malloc(n * 4)
84
+ out_bins = (n / 2) + 1
85
+ out_ptr = fftwf_malloc(out_bins * 2 * 4)
86
+ raise 'ERROR: fftwf_malloc failed' if in_ptr.null? || out_ptr.null?
87
+
88
+ # zero + copy
89
+ in_ptr.write_array_of_float(Array.new(n, 0.0))
90
+ src = samples.first([n, samples.length].min).map(&:to_f)
91
+ in_ptr.write_array_of_float(src + Array.new(n - src.length, 0.0)) if src.length < n
92
+ in_ptr.write_array_of_float(src) if src.length == n
93
+
94
+ plan = fftwf_plan_dft_r2c_1d(n, in_ptr, out_ptr, FFTW_ESTIMATE)
95
+ raise 'ERROR: fftwf_plan_dft_r2c_1d failed' if plan.null?
96
+
97
+ fftwf_execute(plan)
98
+ flat = out_ptr.read_array_of_float(out_bins * 2)
99
+ result = Array.new(out_bins) { |i| [flat[2 * i], flat[(2 * i) + 1]] }
100
+
101
+ fftwf_destroy_plan(plan)
102
+ fftwf_free(in_ptr)
103
+ fftwf_free(out_ptr)
104
+ result
105
+ end
106
+
107
+ # Supported Method Parameters::
108
+ # mag = PWN::FFI::FFTW.rfft_magnitude(
109
+ # samples: 'required - Array<Float>',
110
+ # n: 'optional - FFT size'
111
+ # )
112
+ # Returns Array<Float> of |X[k]| for k=0..n/2.
113
+
114
+ public_class_method def self.rfft_magnitude(opts = {})
115
+ rfft(opts).map { |re, im| Math.sqrt((re * re) + (im * im)) }
116
+ end
117
+
118
+ # Supported Method Parameters::
119
+ # power_db = PWN::FFI::FFTW.rfft_power_db(
120
+ # samples: 'required - Array<Float>',
121
+ # n: 'optional - FFT size',
122
+ # floor: 'optional - dB floor for zeros (default -120.0)'
123
+ # )
124
+ # Returns Array<Float> of 20*log10(|X[k]|) with a noise floor.
125
+
126
+ public_class_method def self.rfft_power_db(opts = {})
127
+ floor = (opts[:floor] || -120.0).to_f
128
+ rfft_magnitude(opts).map do |m|
129
+ m.positive? ? (20.0 * Math.log10(m)) : floor
130
+ end
131
+ end
132
+
133
+ # Supported Method Parameters::
134
+ # spectrum = PWN::FFI::FFTW.cfft(
135
+ # iq: 'required - interleaved Array<Float> I/Q (even length)',
136
+ # n: 'optional - number of complex samples (default iq.length/2)',
137
+ # sign: 'optional - :forward (default) or :backward'
138
+ # )
139
+ # Returns Array of [re, im] pairs length n.
140
+
141
+ public_class_method def self.cfft(opts = {})
142
+ raise 'ERROR: libfftw3f not available' unless available?
143
+
144
+ iq = opts[:iq]
145
+ n = (opts[:n] || (iq.length / 2)).to_i
146
+ raise 'ERROR: n must be >= 1' if n < 1
147
+
148
+ sign = opts[:sign] == :backward ? FFTW_BACKWARD : FFTW_FORWARD
149
+ bytes = n * 2 * 4
150
+ in_ptr = fftwf_malloc(bytes)
151
+ out_ptr = fftwf_malloc(bytes)
152
+ raise 'ERROR: fftwf_malloc failed' if in_ptr.null? || out_ptr.null?
153
+
154
+ src = iq.first([2 * n, iq.length].min).map(&:to_f)
155
+ padded = src + Array.new((2 * n) - src.length, 0.0)
156
+ in_ptr.write_array_of_float(padded)
157
+
158
+ plan = fftwf_plan_dft_1d(n, in_ptr, out_ptr, sign, FFTW_ESTIMATE)
159
+ raise 'ERROR: fftwf_plan_dft_1d failed' if plan.null?
160
+
161
+ fftwf_execute(plan)
162
+ flat = out_ptr.read_array_of_float(n * 2)
163
+ result = Array.new(n) { |i| [flat[2 * i], flat[(2 * i) + 1]] }
164
+
165
+ fftwf_destroy_plan(plan)
166
+ fftwf_free(in_ptr)
167
+ fftwf_free(out_ptr)
168
+ result
169
+ end
170
+
171
+ # Author(s):: 0day Inc. <support@0dayinc.com>
172
+
173
+ public_class_method def self.authors
174
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
175
+ end
176
+
177
+ # Display Usage for this Module
178
+
179
+ public_class_method def self.help
180
+ puts "USAGE:
181
+ #{self}.available? # => true/false
182
+ #{self}.rfft(samples:, n: nil) # real→complex, [[re,im],…]
183
+ #{self}.rfft_magnitude(samples:, n: nil) # |X[k]|
184
+ #{self}.rfft_power_db(samples:, n:, floor: -120.0)
185
+ #{self}.cfft(iq:, n: nil, sign: :forward) # complex FFT
186
+
187
+ #{self}.authors
188
+ "
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
6
+
7
+ module PWN
8
+ module FFI
9
+ # Thin libhackrf binding for inventory / RX of raw I/Q.
10
+ #
11
+ # Intentionally control-plane first: init/open/tune/rate/gains + one-shot
12
+ # sync-style helpers used by Extrospection `probe_rf` and by wideband
13
+ # PWN::SDR::Decoder::* modules that need real I/Q (not GQRX audio).
14
+ # Streaming callbacks stay opt-in — Ruby GC must not run on the
15
+ # libusb transfer thread, so call sites that need continuous RX should
16
+ # buffer into a Queue from a dedicated thread.
17
+ module HackRF
18
+ extend PubFFI::Library
19
+
20
+ HACKRF_SUCCESS = 0
21
+
22
+ @load_error = nil
23
+ begin
24
+ ffi_lib %w[hackrf libhackrf.so.0 libhackrf.so]
25
+ rescue LoadError => e
26
+ @load_error = e
27
+ end
28
+
29
+ class << self
30
+ attr_reader :load_error
31
+ end
32
+
33
+ unless @load_error
34
+ attach_function :hackrf_init, [], :int
35
+ attach_function :hackrf_exit, [], :int
36
+ attach_function :hackrf_library_version, [], :string
37
+ attach_function :hackrf_library_release, [], :string
38
+ attach_function :hackrf_error_name, [:int], :string
39
+
40
+ attach_function :hackrf_open, [:pointer], :int
41
+ attach_function :hackrf_open_by_serial, %i[string pointer], :int
42
+ attach_function :hackrf_close, [:pointer], :int
43
+
44
+ attach_function :hackrf_set_freq, %i[pointer uint64], :int
45
+ attach_function :hackrf_set_sample_rate, %i[pointer double], :int
46
+ attach_function :hackrf_set_baseband_filter_bandwidth, %i[pointer uint32], :int
47
+ attach_function :hackrf_set_vga_gain, %i[pointer uint32], :int
48
+ attach_function :hackrf_set_lna_gain, %i[pointer uint32], :int
49
+ attach_function :hackrf_set_amp_enable, %i[pointer uint8], :int
50
+ attach_function :hackrf_is_streaming, [:pointer], :int
51
+ attach_function :hackrf_board_id_read, %i[pointer pointer], :int
52
+ attach_function :hackrf_board_id_name, [:uint8], :string
53
+ attach_function :hackrf_version_string_read, %i[pointer pointer uint8], :int
54
+ end
55
+
56
+ # Supported Method Parameters::
57
+ # PWN::FFI::HackRF.available?
58
+
59
+ public_class_method def self.available?
60
+ !@load_error && respond_to?(:hackrf_init, true)
61
+ rescue StandardError
62
+ false
63
+ end
64
+
65
+ # Supported Method Parameters::
66
+ # info = PWN::FFI::HackRF.info
67
+ # Returns Hash with library_version / library_release / available.
68
+
69
+ public_class_method def self.info
70
+ return { available: false, error: @load_error&.message } unless available?
71
+
72
+ {
73
+ available: true,
74
+ library_version: hackrf_library_version.to_s,
75
+ library_release: hackrf_library_release.to_s
76
+ }
77
+ end
78
+
79
+ # Supported Method Parameters::
80
+ # dev = PWN::FFI::HackRF.open(serial: nil)
81
+ # Returns FFI::Pointer (opaque hackrf_device*) or raises.
82
+
83
+ public_class_method def self.open(opts = {})
84
+ raise 'ERROR: libhackrf not available' unless available?
85
+
86
+ check!(hackrf_init)
87
+ dev_ptr = PubFFI::MemoryPointer.new(:pointer)
88
+ serial = opts[:serial]
89
+ rc = if serial
90
+ hackrf_open_by_serial(serial.to_s, dev_ptr)
91
+ else
92
+ hackrf_open(dev_ptr)
93
+ end
94
+ check!(rc)
95
+ dev_ptr.read_pointer
96
+ end
97
+
98
+ # Supported Method Parameters::
99
+ # PWN::FFI::HackRF.close(device: pointer)
100
+
101
+ public_class_method def self.close(opts = {})
102
+ dev = opts[:device]
103
+ return unless dev && !dev.null?
104
+
105
+ check!(hackrf_close(dev))
106
+ hackrf_exit
107
+ nil
108
+ end
109
+
110
+ # Supported Method Parameters::
111
+ # PWN::FFI::HackRF.configure(
112
+ # device: 'required - pointer from .open',
113
+ # freq_hz: 'required - center frequency Hz',
114
+ # rate_hz: 'optional - sample rate (default 10e6)',
115
+ # lna_gain: 'optional - 0..40 step 8 (default 16)',
116
+ # vga_gain: 'optional - 0..62 step 2 (default 20)',
117
+ # amp: 'optional - true/false RF amp (default false)',
118
+ # bb_bw_hz: 'optional - baseband filter BW Hz'
119
+ # )
120
+
121
+ public_class_method def self.configure(opts = {}) # rubocop:disable Naming/PredicateMethod
122
+ dev = opts[:device]
123
+ raise 'ERROR: :device required' if dev.nil? || dev.null?
124
+
125
+ freq = opts[:freq_hz].to_i
126
+ rate = (opts[:rate_hz] || 10_000_000).to_f
127
+ lna = (opts[:lna_gain] || 16).to_i
128
+ vga = (opts[:vga_gain] || 20).to_i
129
+ amp = opts[:amp] ? 1 : 0
130
+
131
+ check!(hackrf_set_freq(dev, freq))
132
+ check!(hackrf_set_sample_rate(dev, rate))
133
+ check!(hackrf_set_lna_gain(dev, lna))
134
+ check!(hackrf_set_vga_gain(dev, vga))
135
+ check!(hackrf_set_amp_enable(dev, amp))
136
+ check!(hackrf_set_baseband_filter_bandwidth(dev, opts[:bb_bw_hz].to_i)) if opts[:bb_bw_hz]
137
+ true
138
+ end
139
+
140
+ # Supported Method Parameters::
141
+ # meta = PWN::FFI::HackRF.device_info(device: pointer)
142
+ # Returns { board_id:, board_name:, version: }
143
+
144
+ public_class_method def self.device_info(opts = {})
145
+ dev = opts[:device]
146
+ raise 'ERROR: :device required' if dev.nil? || dev.null?
147
+
148
+ bid_ptr = PubFFI::MemoryPointer.new(:uint8)
149
+ check!(hackrf_board_id_read(dev, bid_ptr))
150
+ bid = bid_ptr.read_uint8
151
+ ver_buf = PubFFI::MemoryPointer.new(:char, 255)
152
+ check!(hackrf_version_string_read(dev, ver_buf, 255))
153
+ {
154
+ board_id: bid,
155
+ board_name: hackrf_board_id_name(bid).to_s,
156
+ version: ver_buf.read_string
157
+ }
158
+ end
159
+
160
+ # Author(s):: 0day Inc. <support@0dayinc.com>
161
+
162
+ public_class_method def self.authors
163
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
164
+ end
165
+
166
+ # Display Usage for this Module
167
+
168
+ public_class_method def self.help
169
+ puts "USAGE:
170
+ #{self}.available?
171
+ #{self}.info
172
+ dev = #{self}.open(serial: nil)
173
+ #{self}.configure(device: dev, freq_hz:, rate_hz: 10e6, lna_gain: 16, vga_gain: 20, amp: false)
174
+ #{self}.device_info(device: dev)
175
+ #{self}.close(device: dev)
176
+
177
+ #{self}.authors
178
+ "
179
+ end
180
+
181
+ class << self
182
+ private
183
+
184
+ def check!(code)
185
+ return if code == HACKRF_SUCCESS
186
+
187
+ name = available? ? hackrf_error_name(code).to_s : code.to_s
188
+ raise "ERROR: libhackrf rc=#{code} (#{name})"
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ PubFFI = ::FFI unless defined?(PubFFI) # rubocop:disable Style/RedundantConstantBase
6
+
7
+ module PWN
8
+ module FFI
9
+ # Thin liquid-dsp binding.
10
+ #
11
+ # Provides the high-level DSP building blocks that PWN::SDR::Decoder::*
12
+ # needs beyond pure-Ruby primitives: FM / frequency demodulation,
13
+ # multi-stage arbitrary resampling, and Kaiser FIR filters. Opaque
14
+ # liquid handles are held as FFI::Pointer and always freed in an
15
+ # ensure — no compile step, no shells. If libliquid is absent
16
+ # `.available?` is false and DSP falls back to pure Ruby.
17
+ #
18
+ # Complex samples use the GCC float _Complex ABI: interleaved
19
+ # float[2] (re, im) per sample, which is how liquid_float_complex is
20
+ # laid out under LIQUID_DEFINE_COMPLEX.
21
+ module Liquid
22
+ extend PubFFI::Library
23
+
24
+ @load_error = nil
25
+ begin
26
+ ffi_lib %w[liquid libliquid.so.1 libliquid.so]
27
+ rescue LoadError => e
28
+ @load_error = e
29
+ end
30
+
31
+ class << self
32
+ attr_reader :load_error
33
+ end
34
+
35
+ unless @load_error
36
+ # ── freqdem (analog FM / frequency demodulator) ────────────────
37
+ attach_function :freqdem_create, [:float], :pointer
38
+ attach_function :freqdem_destroy, [:pointer], :int
39
+ attach_function :freqdem_reset, [:pointer], :int
40
+ attach_function :freqdem_print, [:pointer], :int
41
+ attach_function :freqdem_demodulate_block,
42
+ %i[pointer pointer uint pointer],
43
+ :int
44
+
45
+ # ── msresamp_rrrf (multi-stage arbitrary real resampler) ───────
46
+ attach_function :msresamp_rrrf_create, %i[float float], :pointer
47
+ attach_function :msresamp_rrrf_destroy, [:pointer], :int
48
+ attach_function :msresamp_rrrf_reset, [:pointer], :int
49
+ attach_function :msresamp_rrrf_get_rate, [:pointer], :float
50
+ attach_function :msresamp_rrrf_get_delay, [:pointer], :float
51
+ attach_function :msresamp_rrrf_get_num_output, %i[pointer uint], :uint
52
+ attach_function :msresamp_rrrf_execute,
53
+ %i[pointer pointer uint pointer pointer],
54
+ :int
55
+
56
+ # ── firfilt_rrrf (real FIR filter) ─────────────────────────────
57
+ attach_function :firfilt_rrrf_create_kaiser,
58
+ %i[uint float float float],
59
+ :pointer
60
+ attach_function :firfilt_rrrf_create_dc_blocker,
61
+ %i[uint float],
62
+ :pointer
63
+ attach_function :firfilt_rrrf_destroy, [:pointer], :int
64
+ attach_function :firfilt_rrrf_reset, [:pointer], :int
65
+ attach_function :firfilt_rrrf_execute_block,
66
+ %i[pointer pointer uint pointer],
67
+ :int
68
+ end
69
+
70
+ # Supported Method Parameters::
71
+ # PWN::FFI::Liquid.available?
72
+
73
+ public_class_method def self.available?
74
+ !@load_error && respond_to?(:freqdem_create, true)
75
+ rescue StandardError
76
+ false
77
+ end
78
+
79
+ # Supported Method Parameters::
80
+ # audio = PWN::FFI::Liquid.freq_demod(
81
+ # iq: 'required - interleaved I/Q Array<Float> (even length)',
82
+ # kf: 'optional - modulation factor (default 0.5)'
83
+ # )
84
+ # Returns Array<Float> of demodulated real samples (length = iq.length/2).
85
+
86
+ public_class_method def self.freq_demod(opts = {})
87
+ raise 'ERROR: libliquid not available' unless available?
88
+
89
+ iq = opts[:iq]
90
+ kf = (opts[:kf] || 0.5).to_f
91
+ n = iq.length / 2
92
+ return [] if n.zero?
93
+
94
+ fd = freqdem_create(kf)
95
+ raise 'ERROR: freqdem_create failed' if fd.null?
96
+
97
+ begin
98
+ in_ptr = float_ptr(iq.first(n * 2))
99
+ out_ptr = PubFFI::MemoryPointer.new(:float, n)
100
+ rc = freqdem_demodulate_block(fd, in_ptr, n, out_ptr)
101
+ raise "ERROR: freqdem_demodulate_block rc=#{rc}" unless rc.zero?
102
+
103
+ out_ptr.read_array_of_float(n)
104
+ ensure
105
+ freqdem_destroy(fd)
106
+ end
107
+ end
108
+
109
+ # Supported Method Parameters::
110
+ # out = PWN::FFI::Liquid.resample(
111
+ # samples: 'required - Array<Float>',
112
+ # rate: 'required - output/input ratio (>0)',
113
+ # as_db: 'optional - stop-band attenuation dB (default 60.0)'
114
+ # )
115
+ # Returns Array<Float> of resampled samples.
116
+
117
+ public_class_method def self.resample(opts = {})
118
+ raise 'ERROR: libliquid not available' unless available?
119
+
120
+ samples = opts[:samples]
121
+ rate = opts[:rate].to_f
122
+ as_db = (opts[:as_db] || 60.0).to_f
123
+ raise 'ERROR: rate must be > 0' unless rate.positive?
124
+ return samples.dup if samples.empty?
125
+
126
+ q = msresamp_rrrf_create(rate, as_db)
127
+ raise 'ERROR: msresamp_rrrf_create failed' if q.null?
128
+
129
+ begin
130
+ nx = samples.length
131
+ # allocate generously: ceil(1 + 2*r*nx)
132
+ ny_max = [1, (1 + (2 * rate * nx)).ceil].max
133
+ in_ptr = float_ptr(samples)
134
+ out_ptr = PubFFI::MemoryPointer.new(:float, ny_max)
135
+ ny_ptr = PubFFI::MemoryPointer.new(:uint, 1)
136
+ rc = msresamp_rrrf_execute(q, in_ptr, nx, out_ptr, ny_ptr)
137
+ raise "ERROR: msresamp_rrrf_execute rc=#{rc}" unless rc.zero?
138
+
139
+ ny = ny_ptr.read_uint
140
+ out_ptr.read_array_of_float(ny)
141
+ ensure
142
+ msresamp_rrrf_destroy(q)
143
+ end
144
+ end
145
+
146
+ # Supported Method Parameters::
147
+ # out = PWN::FFI::Liquid.fir_kaiser(
148
+ # samples: 'required - Array<Float>',
149
+ # length: 'optional - filter length (default 51, odd preferred)',
150
+ # fc: 'optional - normalised cutoff 0..0.5 (default 0.1)',
151
+ # as_db: 'optional - stop-band attenuation dB (default 60.0)',
152
+ # mu: 'optional - fractional sample offset (default 0.0)'
153
+ # )
154
+ # Returns Array<Float> same length as input.
155
+
156
+ public_class_method def self.fir_kaiser(opts = {})
157
+ raise 'ERROR: libliquid not available' unless available?
158
+
159
+ samples = opts[:samples]
160
+ length = (opts[:length] || 51).to_i
161
+ fc = (opts[:fc] || 0.1).to_f
162
+ as_db = (opts[:as_db] || 60.0).to_f
163
+ mu = (opts[:mu] || 0.0).to_f
164
+ return [] if samples.empty?
165
+
166
+ q = firfilt_rrrf_create_kaiser(length, fc, as_db, mu)
167
+ raise 'ERROR: firfilt_rrrf_create_kaiser failed' if q.null?
168
+
169
+ begin
170
+ n = samples.length
171
+ in_ptr = float_ptr(samples)
172
+ out_ptr = PubFFI::MemoryPointer.new(:float, n)
173
+ rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
174
+ raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?
175
+
176
+ out_ptr.read_array_of_float(n)
177
+ ensure
178
+ firfilt_rrrf_destroy(q)
179
+ end
180
+ end
181
+
182
+ # Supported Method Parameters::
183
+ # out = PWN::FFI::Liquid.dc_block(
184
+ # samples: 'required - Array<Float>',
185
+ # m: 'optional - prototype semi-length (default 7 → len 15)',
186
+ # as_db: 'optional - stop-band attenuation dB (default 60.0)'
187
+ # )
188
+
189
+ public_class_method def self.dc_block(opts = {})
190
+ raise 'ERROR: libliquid not available' unless available?
191
+
192
+ samples = opts[:samples]
193
+ m = (opts[:m] || 7).to_i
194
+ as_db = (opts[:as_db] || 60.0).to_f
195
+ return [] if samples.empty?
196
+
197
+ q = firfilt_rrrf_create_dc_blocker(m, as_db)
198
+ raise 'ERROR: firfilt_rrrf_create_dc_blocker failed' if q.null?
199
+
200
+ begin
201
+ n = samples.length
202
+ in_ptr = float_ptr(samples)
203
+ out_ptr = PubFFI::MemoryPointer.new(:float, n)
204
+ rc = firfilt_rrrf_execute_block(q, in_ptr, n, out_ptr)
205
+ raise "ERROR: firfilt_rrrf_execute_block rc=#{rc}" unless rc.zero?
206
+
207
+ out_ptr.read_array_of_float(n)
208
+ ensure
209
+ firfilt_rrrf_destroy(q)
210
+ end
211
+ end
212
+
213
+ # Author(s):: 0day Inc. <support@0dayinc.com>
214
+
215
+ public_class_method def self.authors
216
+ "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
217
+ end
218
+
219
+ # Display Usage for this Module
220
+
221
+ public_class_method def self.help
222
+ puts "USAGE:
223
+ #{self}.available? # => true/false
224
+ #{self}.freq_demod(iq:, kf: 0.5) # FM I/Q → audio
225
+ #{self}.resample(samples:, rate:, as_db: 60.0) # arbitrary rate
226
+ #{self}.fir_kaiser(samples:, length: 51, fc: 0.1, as_db: 60.0)
227
+ #{self}.dc_block(samples:, m: 7, as_db: 60.0)
228
+
229
+ #{self}.authors
230
+ "
231
+ end
232
+
233
+ class << self
234
+ private
235
+
236
+ def float_ptr(arr)
237
+ ptr = PubFFI::MemoryPointer.new(:float, arr.length)
238
+ ptr.write_array_of_float(arr.map(&:to_f))
239
+ ptr
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end