oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""CoAP option definitions and parsing helpers.
|
|
2
|
+
|
|
3
|
+
Provides option number to name mappings, content format definitions,
|
|
4
|
+
and helper functions for decoding CoAP options.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
RFC 7252 Section 5.10: Option Definitions
|
|
8
|
+
RFC 7252 Section 12.3: CoAP Content-Formats Registry
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, ClassVar
|
|
14
|
+
|
|
15
|
+
# CoAP option number to name mapping (RFC 7252)
|
|
16
|
+
OPTIONS: dict[int, str] = {
|
|
17
|
+
1: "If-Match",
|
|
18
|
+
3: "Uri-Host",
|
|
19
|
+
4: "ETag",
|
|
20
|
+
5: "If-None-Match",
|
|
21
|
+
6: "Observe", # RFC 7641
|
|
22
|
+
7: "Uri-Port",
|
|
23
|
+
8: "Location-Path",
|
|
24
|
+
11: "Uri-Path",
|
|
25
|
+
12: "Content-Format",
|
|
26
|
+
14: "Max-Age",
|
|
27
|
+
15: "Uri-Query",
|
|
28
|
+
17: "Accept",
|
|
29
|
+
20: "Location-Query",
|
|
30
|
+
23: "Block2", # RFC 7959
|
|
31
|
+
27: "Block1", # RFC 7959
|
|
32
|
+
28: "Size2", # RFC 7959
|
|
33
|
+
35: "Proxy-Uri",
|
|
34
|
+
39: "Proxy-Scheme",
|
|
35
|
+
60: "Size1", # RFC 7959
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# CoAP content format codes (RFC 7252)
|
|
39
|
+
CONTENT_FORMATS: dict[int, str] = {
|
|
40
|
+
0: "text/plain; charset=utf-8",
|
|
41
|
+
40: "application/link-format",
|
|
42
|
+
41: "application/xml",
|
|
43
|
+
42: "application/octet-stream",
|
|
44
|
+
47: "application/exi",
|
|
45
|
+
50: "application/json",
|
|
46
|
+
60: "application/cbor",
|
|
47
|
+
# Additional formats from IANA registry
|
|
48
|
+
100: "application/senml+json",
|
|
49
|
+
101: "application/sensml+json",
|
|
50
|
+
110: "application/senml+cbor",
|
|
51
|
+
111: "application/sensml+cbor",
|
|
52
|
+
112: "application/senml-exi",
|
|
53
|
+
113: "application/sensml-exi",
|
|
54
|
+
256: "application/coap-group+json",
|
|
55
|
+
10000: "application/pkcs7-mime; smime-type=server-generated-key",
|
|
56
|
+
10001: "application/pkcs7-mime; smime-type=certs-only",
|
|
57
|
+
10002: "application/pkcs8",
|
|
58
|
+
10003: "application/csrattrs",
|
|
59
|
+
10004: "application/pkcs10",
|
|
60
|
+
10005: "application/pkix-cert",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class OptionParser:
|
|
65
|
+
"""Helper class for parsing CoAP options.
|
|
66
|
+
|
|
67
|
+
Provides methods for decoding option values based on their option number
|
|
68
|
+
and handling CoAP option delta encoding.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> parser = OptionParser()
|
|
72
|
+
>>> value = parser.decode_value(11, b"temperature")
|
|
73
|
+
>>> print(value)
|
|
74
|
+
'temperature'
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# Empty format options (no value)
|
|
78
|
+
EMPTY_OPTIONS: ClassVar[set[int]] = {5} # If-None-Match
|
|
79
|
+
|
|
80
|
+
# Opaque options (bytes)
|
|
81
|
+
OPAQUE_OPTIONS: ClassVar[set[int]] = {1, 4} # If-Match, ETag
|
|
82
|
+
|
|
83
|
+
# String options (UTF-8)
|
|
84
|
+
STRING_OPTIONS: ClassVar[set[int]] = {3, 8, 11, 15, 20, 35, 39}
|
|
85
|
+
|
|
86
|
+
# Unsigned integer options
|
|
87
|
+
UINT_OPTIONS: ClassVar[set[int]] = {6, 7, 12, 14, 17, 23, 27, 28, 60}
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def decode_value(option_num: int, value: bytes) -> str | int | bytes:
|
|
91
|
+
"""Decode option value based on option number.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
option_num: CoAP option number.
|
|
95
|
+
value: Raw option value bytes.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Decoded value as string, int, or bytes depending on option type.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> parser = OptionParser()
|
|
102
|
+
>>> parser.decode_value(11, b"sensor")
|
|
103
|
+
'sensor'
|
|
104
|
+
>>> parser.decode_value(12, b"\\x00\\x32")
|
|
105
|
+
50
|
|
106
|
+
"""
|
|
107
|
+
if option_num in OptionParser.EMPTY_OPTIONS:
|
|
108
|
+
return b""
|
|
109
|
+
|
|
110
|
+
if not value:
|
|
111
|
+
# Empty value - return appropriate type
|
|
112
|
+
if option_num in OptionParser.STRING_OPTIONS:
|
|
113
|
+
return ""
|
|
114
|
+
if option_num in OptionParser.UINT_OPTIONS:
|
|
115
|
+
return 0
|
|
116
|
+
return b""
|
|
117
|
+
|
|
118
|
+
if option_num in OptionParser.STRING_OPTIONS:
|
|
119
|
+
try:
|
|
120
|
+
return value.decode("utf-8")
|
|
121
|
+
except UnicodeDecodeError:
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
if option_num in OptionParser.UINT_OPTIONS:
|
|
125
|
+
return int.from_bytes(value, "big")
|
|
126
|
+
|
|
127
|
+
# Default to opaque (bytes)
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def parse_extended_value(base: int, data: bytes, offset: int) -> tuple[int, int]:
|
|
132
|
+
"""Parse extended delta or length value.
|
|
133
|
+
|
|
134
|
+
CoAP uses extended encoding for delta/length values >= 13:
|
|
135
|
+
- 13: value is (base value from next byte) + 13
|
|
136
|
+
- 14: value is (base value from next 2 bytes) + 269
|
|
137
|
+
- 15: reserved (payload marker or error)
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
base: Base value (0-15) from option header nibble.
|
|
141
|
+
data: Complete message data.
|
|
142
|
+
offset: Current offset in data.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tuple of (actual_value, bytes_consumed).
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If extended format is invalid or data insufficient.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> parser = OptionParser()
|
|
152
|
+
>>> value, consumed = parser.parse_extended_value(13, b"\\x05\\x00", 0)
|
|
153
|
+
>>> value, consumed
|
|
154
|
+
(18, 1)
|
|
155
|
+
"""
|
|
156
|
+
if base < 13:
|
|
157
|
+
return base, 0
|
|
158
|
+
|
|
159
|
+
if base == 13:
|
|
160
|
+
if len(data) < offset + 1:
|
|
161
|
+
raise ValueError("Insufficient data for extended option delta/length")
|
|
162
|
+
return data[offset] + 13, 1
|
|
163
|
+
|
|
164
|
+
if base == 14:
|
|
165
|
+
if len(data) < offset + 2:
|
|
166
|
+
raise ValueError("Insufficient data for extended option delta/length")
|
|
167
|
+
return int.from_bytes(data[offset : offset + 2], "big") + 269, 2
|
|
168
|
+
|
|
169
|
+
# base == 15
|
|
170
|
+
raise ValueError("Invalid option delta/length value (15)")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def format_block_option(value: int) -> dict[str, Any]:
|
|
174
|
+
"""Parse Block1/Block2 option value.
|
|
175
|
+
|
|
176
|
+
Block options encode: NUM (block number), M (more flag), SZX (size exponent).
|
|
177
|
+
|
|
178
|
+
Format (variable length, 1-3 bytes):
|
|
179
|
+
0 1 2 3 4 5 6 7
|
|
180
|
+
+-+-+-+-+-+-+-+-+
|
|
181
|
+
| NUM |M| SZX |
|
|
182
|
+
+-+-+-+-+-+-+-+-+
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
value: Block option value as unsigned integer.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary with 'num' (block number), 'more' (more flag), and 'size' (block size).
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> result = format_block_option(0x08) # Block 0, no more, size 16
|
|
192
|
+
>>> result['num'], result['more'], result['size']
|
|
193
|
+
(0, False, 16)
|
|
194
|
+
"""
|
|
195
|
+
num = value >> 4
|
|
196
|
+
more = bool((value >> 3) & 0x01)
|
|
197
|
+
szx = value & 0x07
|
|
198
|
+
size = 2 ** (szx + 4) # Size = 2^(SZX+4), range: 16-1024 bytes
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"num": num,
|
|
202
|
+
"more": more,
|
|
203
|
+
"size": size,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
__all__ = [
|
|
208
|
+
"CONTENT_FORMATS",
|
|
209
|
+
"OPTIONS",
|
|
210
|
+
"OptionParser",
|
|
211
|
+
"format_block_option",
|
|
212
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""LoRaWAN protocol decoder package.
|
|
2
|
+
|
|
3
|
+
Provides LoRaWAN MAC layer parsing and payload decryption support.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from oscura.iot.lorawan.decoder import (
|
|
7
|
+
LoRaWANDecoder,
|
|
8
|
+
LoRaWANFrame,
|
|
9
|
+
LoRaWANKeys,
|
|
10
|
+
decode_lorawan_frame,
|
|
11
|
+
)
|
|
12
|
+
from oscura.iot.lorawan.mac_commands import MAC_COMMANDS, parse_mac_command
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MAC_COMMANDS",
|
|
16
|
+
"LoRaWANDecoder",
|
|
17
|
+
"LoRaWANFrame",
|
|
18
|
+
"LoRaWANKeys",
|
|
19
|
+
"decode_lorawan_frame",
|
|
20
|
+
"parse_mac_command",
|
|
21
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""LoRaWAN cryptographic operations.
|
|
2
|
+
|
|
3
|
+
This module provides AES-128 encryption/decryption and CMAC operations
|
|
4
|
+
for LoRaWAN payload security as defined in LoRaWAN Specification 1.0.3.
|
|
5
|
+
|
|
6
|
+
Security:
|
|
7
|
+
Uses cryptography library (modern, actively maintained) instead of deprecated PyCrypto.
|
|
8
|
+
All cryptographic operations follow LoRaWAN 1.0.3 specification.
|
|
9
|
+
|
|
10
|
+
References:
|
|
11
|
+
LoRaWAN Specification 1.0.3: https://lora-alliance.org/resource_hub/lorawan-specification-v1-0-3/
|
|
12
|
+
Section 4.3.3 - MAC Frame Payload Encryption (FRMPayload)
|
|
13
|
+
Section 4.4 - Message Integrity Code (MIC)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def decrypt_payload(
|
|
22
|
+
frm_payload: bytes,
|
|
23
|
+
key: bytes,
|
|
24
|
+
dev_addr: int,
|
|
25
|
+
fcnt: int,
|
|
26
|
+
direction: Literal["up", "down"],
|
|
27
|
+
) -> bytes:
|
|
28
|
+
"""Decrypt LoRaWAN FRMPayload using AES-128 in CTR mode.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
frm_payload: Encrypted payload bytes.
|
|
32
|
+
key: AES-128 key (16 bytes) - AppSKey or NwkSKey.
|
|
33
|
+
dev_addr: Device address (4 bytes).
|
|
34
|
+
fcnt: Frame counter.
|
|
35
|
+
direction: Direction "up" (uplink) or "down" (downlink).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Decrypted payload bytes.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If key is not 16 bytes.
|
|
42
|
+
ImportError: If cryptography library is not available.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> key = bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C")
|
|
46
|
+
>>> encrypted = bytes.fromhex("0123456789ABCDEF")
|
|
47
|
+
>>> decrypted = decrypt_payload(encrypted, key, 0x01020304, 1, "up")
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
from cryptography.hazmat.backends import default_backend
|
|
51
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
msg = (
|
|
54
|
+
"cryptography library is required for LoRaWAN encryption. "
|
|
55
|
+
"Install with: pip install cryptography"
|
|
56
|
+
)
|
|
57
|
+
raise ImportError(msg) from exc
|
|
58
|
+
|
|
59
|
+
if len(key) != 16:
|
|
60
|
+
msg = f"Key must be 16 bytes, got {len(key)}"
|
|
61
|
+
raise ValueError(msg)
|
|
62
|
+
|
|
63
|
+
if len(frm_payload) == 0:
|
|
64
|
+
return b""
|
|
65
|
+
|
|
66
|
+
# Direction byte: 0x00 for uplink, 0x01 for downlink
|
|
67
|
+
dir_byte = 0x00 if direction == "up" else 0x01
|
|
68
|
+
|
|
69
|
+
# Number of 16-byte blocks needed
|
|
70
|
+
num_blocks = (len(frm_payload) + 15) // 16
|
|
71
|
+
|
|
72
|
+
# Generate keystream by encrypting counter blocks
|
|
73
|
+
keystream = b""
|
|
74
|
+
for i in range(num_blocks):
|
|
75
|
+
# Build encryption block A_i (16 bytes)
|
|
76
|
+
# A_i = 0x01 | 0x00000000 | Dir | DevAddr | FCnt | 0x00 | i
|
|
77
|
+
a = bytearray(16)
|
|
78
|
+
a[0] = 0x01 # Encryption flag
|
|
79
|
+
# a[1:5] = 0x00000000 (already zero)
|
|
80
|
+
a[5] = dir_byte
|
|
81
|
+
a[6:10] = dev_addr.to_bytes(4, "little")
|
|
82
|
+
a[10:14] = fcnt.to_bytes(4, "little")
|
|
83
|
+
# a[14] = 0x00 (already zero)
|
|
84
|
+
a[15] = i + 1
|
|
85
|
+
|
|
86
|
+
# Encrypt the block using AES in ECB mode
|
|
87
|
+
# Security: ECB mode is required by LoRaWAN spec (Section 4.3.3)
|
|
88
|
+
# Each counter block is unique, so ECB mode is safe here
|
|
89
|
+
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
90
|
+
encryptor = cipher.encryptor()
|
|
91
|
+
keystream += encryptor.update(bytes(a)) + encryptor.finalize()
|
|
92
|
+
|
|
93
|
+
# XOR payload with keystream
|
|
94
|
+
decrypted = bytes(
|
|
95
|
+
p ^ k for p, k in zip(frm_payload, keystream[: len(frm_payload)], strict=False)
|
|
96
|
+
)
|
|
97
|
+
return decrypted
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def compute_mic(
|
|
101
|
+
data: bytes,
|
|
102
|
+
key: bytes,
|
|
103
|
+
dev_addr: int,
|
|
104
|
+
fcnt: int,
|
|
105
|
+
direction: Literal["up", "down"],
|
|
106
|
+
) -> int:
|
|
107
|
+
"""Compute LoRaWAN Message Integrity Code (MIC) using AES-CMAC.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
data: Message data (MHDR | FHDR | FPort | FRMPayload).
|
|
111
|
+
key: Network session key (NwkSKey, 16 bytes).
|
|
112
|
+
dev_addr: Device address (4 bytes).
|
|
113
|
+
fcnt: Frame counter.
|
|
114
|
+
direction: Direction "up" (uplink) or "down" (downlink).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
32-bit MIC value.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If key is not 16 bytes.
|
|
121
|
+
ImportError: If cryptography library is not available.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> key = bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C")
|
|
125
|
+
>>> data = bytes.fromhex("40010203040001000100")
|
|
126
|
+
>>> mic = compute_mic(data, key, 0x01020304, 1, "up")
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
from cryptography.hazmat.backends import default_backend
|
|
130
|
+
from cryptography.hazmat.primitives import cmac
|
|
131
|
+
from cryptography.hazmat.primitives.ciphers import algorithms
|
|
132
|
+
except ImportError as exc:
|
|
133
|
+
msg = (
|
|
134
|
+
"cryptography library is required for LoRaWAN MIC computation. "
|
|
135
|
+
"Install with: pip install cryptography"
|
|
136
|
+
)
|
|
137
|
+
raise ImportError(msg) from exc
|
|
138
|
+
|
|
139
|
+
if len(key) != 16:
|
|
140
|
+
msg = f"Key must be 16 bytes, got {len(key)}"
|
|
141
|
+
raise ValueError(msg)
|
|
142
|
+
|
|
143
|
+
# Direction byte: 0x00 for uplink, 0x01 for downlink
|
|
144
|
+
dir_byte = 0x00 if direction == "up" else 0x01
|
|
145
|
+
|
|
146
|
+
# Build MIC computation block B_0 (16 bytes)
|
|
147
|
+
# B_0 = 0x49 | 0x00000000 | Dir | DevAddr | FCnt | 0x00 | len(msg)
|
|
148
|
+
b0 = bytearray(16)
|
|
149
|
+
b0[0] = 0x49 # MIC flag
|
|
150
|
+
# b0[1:5] = 0x00000000 (already zero)
|
|
151
|
+
b0[5] = dir_byte
|
|
152
|
+
b0[6:10] = dev_addr.to_bytes(4, "little")
|
|
153
|
+
b0[10:14] = fcnt.to_bytes(4, "little")
|
|
154
|
+
# b0[14] = 0x00 (already zero)
|
|
155
|
+
b0[15] = len(data)
|
|
156
|
+
|
|
157
|
+
# Compute CMAC over B_0 | msg
|
|
158
|
+
c = cmac.CMAC(algorithms.AES(key), backend=default_backend())
|
|
159
|
+
c.update(bytes(b0))
|
|
160
|
+
c.update(data)
|
|
161
|
+
mac = c.finalize()
|
|
162
|
+
|
|
163
|
+
# Return first 4 bytes as 32-bit integer (little-endian)
|
|
164
|
+
mic = int.from_bytes(mac[:4], "little")
|
|
165
|
+
return mic
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def verify_mic(
|
|
169
|
+
data: bytes,
|
|
170
|
+
received_mic: int,
|
|
171
|
+
key: bytes,
|
|
172
|
+
dev_addr: int,
|
|
173
|
+
fcnt: int,
|
|
174
|
+
direction: Literal["up", "down"],
|
|
175
|
+
) -> bool:
|
|
176
|
+
"""Verify LoRaWAN Message Integrity Code (MIC).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
data: Message data (MHDR | FHDR | FPort | FRMPayload).
|
|
180
|
+
received_mic: Received MIC value.
|
|
181
|
+
key: Network session key (NwkSKey, 16 bytes).
|
|
182
|
+
dev_addr: Device address (4 bytes).
|
|
183
|
+
fcnt: Frame counter.
|
|
184
|
+
direction: Direction "up" (uplink) or "down" (downlink).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if MIC is valid, False otherwise.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> key = bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C")
|
|
191
|
+
>>> data = bytes.fromhex("40010203040001000100")
|
|
192
|
+
>>> is_valid = verify_mic(data, 0x12345678, key, 0x01020304, 1, "up")
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
computed_mic = compute_mic(data, key, dev_addr, fcnt, direction)
|
|
196
|
+
return computed_mic == received_mic
|
|
197
|
+
except (ImportError, ValueError):
|
|
198
|
+
# If crypto is unavailable, we can't verify
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"compute_mic",
|
|
204
|
+
"decrypt_payload",
|
|
205
|
+
"verify_mic",
|
|
206
|
+
]
|