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,1025 @@
|
|
|
1
|
+
"""Differential Power Analysis (DPA) framework for side-channel attacks.
|
|
2
|
+
|
|
3
|
+
This module implements DPA, CPA (Correlation Power Analysis), and Template attacks
|
|
4
|
+
for recovering cryptographic keys from power consumption traces. Supports AES and DES
|
|
5
|
+
with multiple leakage models.
|
|
6
|
+
|
|
7
|
+
Key capabilities:
|
|
8
|
+
- DPA attack using difference of means
|
|
9
|
+
- CPA attack using Pearson correlation
|
|
10
|
+
- Template attack with profiling and matching phases
|
|
11
|
+
- Multiple leakage models (Hamming weight, Hamming distance, identity)
|
|
12
|
+
- AES S-box and DES operations
|
|
13
|
+
- Visualization of correlation traces and key rankings
|
|
14
|
+
- JSON export of attack results
|
|
15
|
+
|
|
16
|
+
Typical use cases:
|
|
17
|
+
- Break AES-128 implementations using power analysis
|
|
18
|
+
- Recover DES keys from embedded devices
|
|
19
|
+
- Evaluate cryptographic implementation security
|
|
20
|
+
- Generate attack visualizations for reports
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> from oscura.side_channel.dpa import DPAAnalyzer, PowerTrace
|
|
24
|
+
>>> import numpy as np
|
|
25
|
+
>>> # Create analyzer
|
|
26
|
+
>>> analyzer = DPAAnalyzer(attack_type="cpa", leakage_model="hamming_weight")
|
|
27
|
+
>>> # Load power traces with plaintexts
|
|
28
|
+
>>> traces = [
|
|
29
|
+
... PowerTrace(
|
|
30
|
+
... timestamp=np.arange(1000),
|
|
31
|
+
... power=np.random.randn(1000),
|
|
32
|
+
... plaintext=bytes([i % 256 for i in range(16)])
|
|
33
|
+
... )
|
|
34
|
+
... for _ in range(100)
|
|
35
|
+
... ]
|
|
36
|
+
>>> # Perform attack on first key byte
|
|
37
|
+
>>> result = analyzer.perform_attack(traces, target_byte=0, algorithm="aes128")
|
|
38
|
+
>>> print(f"Recovered key byte: 0x{result.recovered_key[0]:02X}")
|
|
39
|
+
>>> print(f"Confidence: {result.confidence:.2%}")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import logging
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from collections.abc import Sequence
|
|
54
|
+
|
|
55
|
+
from numpy.typing import NDArray
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# AES S-box (forward substitution box)
|
|
61
|
+
AES_SBOX = [
|
|
62
|
+
0x63,
|
|
63
|
+
0x7C,
|
|
64
|
+
0x77,
|
|
65
|
+
0x7B,
|
|
66
|
+
0xF2,
|
|
67
|
+
0x6B,
|
|
68
|
+
0x6F,
|
|
69
|
+
0xC5,
|
|
70
|
+
0x30,
|
|
71
|
+
0x01,
|
|
72
|
+
0x67,
|
|
73
|
+
0x2B,
|
|
74
|
+
0xFE,
|
|
75
|
+
0xD7,
|
|
76
|
+
0xAB,
|
|
77
|
+
0x76,
|
|
78
|
+
0xCA,
|
|
79
|
+
0x82,
|
|
80
|
+
0xC9,
|
|
81
|
+
0x7D,
|
|
82
|
+
0xFA,
|
|
83
|
+
0x59,
|
|
84
|
+
0x47,
|
|
85
|
+
0xF0,
|
|
86
|
+
0xAD,
|
|
87
|
+
0xD4,
|
|
88
|
+
0xA2,
|
|
89
|
+
0xAF,
|
|
90
|
+
0x9C,
|
|
91
|
+
0xA4,
|
|
92
|
+
0x72,
|
|
93
|
+
0xC0,
|
|
94
|
+
0xB7,
|
|
95
|
+
0xFD,
|
|
96
|
+
0x93,
|
|
97
|
+
0x26,
|
|
98
|
+
0x36,
|
|
99
|
+
0x3F,
|
|
100
|
+
0xF7,
|
|
101
|
+
0xCC,
|
|
102
|
+
0x34,
|
|
103
|
+
0xA5,
|
|
104
|
+
0xE5,
|
|
105
|
+
0xF1,
|
|
106
|
+
0x71,
|
|
107
|
+
0xD8,
|
|
108
|
+
0x31,
|
|
109
|
+
0x15,
|
|
110
|
+
0x04,
|
|
111
|
+
0xC7,
|
|
112
|
+
0x23,
|
|
113
|
+
0xC3,
|
|
114
|
+
0x18,
|
|
115
|
+
0x96,
|
|
116
|
+
0x05,
|
|
117
|
+
0x9A,
|
|
118
|
+
0x07,
|
|
119
|
+
0x12,
|
|
120
|
+
0x80,
|
|
121
|
+
0xE2,
|
|
122
|
+
0xEB,
|
|
123
|
+
0x27,
|
|
124
|
+
0xB2,
|
|
125
|
+
0x75,
|
|
126
|
+
0x09,
|
|
127
|
+
0x83,
|
|
128
|
+
0x2C,
|
|
129
|
+
0x1A,
|
|
130
|
+
0x1B,
|
|
131
|
+
0x6E,
|
|
132
|
+
0x5A,
|
|
133
|
+
0xA0,
|
|
134
|
+
0x52,
|
|
135
|
+
0x3B,
|
|
136
|
+
0xD6,
|
|
137
|
+
0xB3,
|
|
138
|
+
0x29,
|
|
139
|
+
0xE3,
|
|
140
|
+
0x2F,
|
|
141
|
+
0x84,
|
|
142
|
+
0x53,
|
|
143
|
+
0xD1,
|
|
144
|
+
0x00,
|
|
145
|
+
0xED,
|
|
146
|
+
0x20,
|
|
147
|
+
0xFC,
|
|
148
|
+
0xB1,
|
|
149
|
+
0x5B,
|
|
150
|
+
0x6A,
|
|
151
|
+
0xCB,
|
|
152
|
+
0xBE,
|
|
153
|
+
0x39,
|
|
154
|
+
0x4A,
|
|
155
|
+
0x4C,
|
|
156
|
+
0x58,
|
|
157
|
+
0xCF,
|
|
158
|
+
0xD0,
|
|
159
|
+
0xEF,
|
|
160
|
+
0xAA,
|
|
161
|
+
0xFB,
|
|
162
|
+
0x43,
|
|
163
|
+
0x4D,
|
|
164
|
+
0x33,
|
|
165
|
+
0x85,
|
|
166
|
+
0x45,
|
|
167
|
+
0xF9,
|
|
168
|
+
0x02,
|
|
169
|
+
0x7F,
|
|
170
|
+
0x50,
|
|
171
|
+
0x3C,
|
|
172
|
+
0x9F,
|
|
173
|
+
0xA8,
|
|
174
|
+
0x51,
|
|
175
|
+
0xA3,
|
|
176
|
+
0x40,
|
|
177
|
+
0x8F,
|
|
178
|
+
0x92,
|
|
179
|
+
0x9D,
|
|
180
|
+
0x38,
|
|
181
|
+
0xF5,
|
|
182
|
+
0xBC,
|
|
183
|
+
0xB6,
|
|
184
|
+
0xDA,
|
|
185
|
+
0x21,
|
|
186
|
+
0x10,
|
|
187
|
+
0xFF,
|
|
188
|
+
0xF3,
|
|
189
|
+
0xD2,
|
|
190
|
+
0xCD,
|
|
191
|
+
0x0C,
|
|
192
|
+
0x13,
|
|
193
|
+
0xEC,
|
|
194
|
+
0x5F,
|
|
195
|
+
0x97,
|
|
196
|
+
0x44,
|
|
197
|
+
0x17,
|
|
198
|
+
0xC4,
|
|
199
|
+
0xA7,
|
|
200
|
+
0x7E,
|
|
201
|
+
0x3D,
|
|
202
|
+
0x64,
|
|
203
|
+
0x5D,
|
|
204
|
+
0x19,
|
|
205
|
+
0x73,
|
|
206
|
+
0x60,
|
|
207
|
+
0x81,
|
|
208
|
+
0x4F,
|
|
209
|
+
0xDC,
|
|
210
|
+
0x22,
|
|
211
|
+
0x2A,
|
|
212
|
+
0x90,
|
|
213
|
+
0x88,
|
|
214
|
+
0x46,
|
|
215
|
+
0xEE,
|
|
216
|
+
0xB8,
|
|
217
|
+
0x14,
|
|
218
|
+
0xDE,
|
|
219
|
+
0x5E,
|
|
220
|
+
0x0B,
|
|
221
|
+
0xDB,
|
|
222
|
+
0xE0,
|
|
223
|
+
0x32,
|
|
224
|
+
0x3A,
|
|
225
|
+
0x0A,
|
|
226
|
+
0x49,
|
|
227
|
+
0x06,
|
|
228
|
+
0x24,
|
|
229
|
+
0x5C,
|
|
230
|
+
0xC2,
|
|
231
|
+
0xD3,
|
|
232
|
+
0xAC,
|
|
233
|
+
0x62,
|
|
234
|
+
0x91,
|
|
235
|
+
0x95,
|
|
236
|
+
0xE4,
|
|
237
|
+
0x79,
|
|
238
|
+
0xE7,
|
|
239
|
+
0xC8,
|
|
240
|
+
0x37,
|
|
241
|
+
0x6D,
|
|
242
|
+
0x8D,
|
|
243
|
+
0xD5,
|
|
244
|
+
0x4E,
|
|
245
|
+
0xA9,
|
|
246
|
+
0x6C,
|
|
247
|
+
0x56,
|
|
248
|
+
0xF4,
|
|
249
|
+
0xEA,
|
|
250
|
+
0x65,
|
|
251
|
+
0x7A,
|
|
252
|
+
0xAE,
|
|
253
|
+
0x08,
|
|
254
|
+
0xBA,
|
|
255
|
+
0x78,
|
|
256
|
+
0x25,
|
|
257
|
+
0x2E,
|
|
258
|
+
0x1C,
|
|
259
|
+
0xA6,
|
|
260
|
+
0xB4,
|
|
261
|
+
0xC6,
|
|
262
|
+
0xE8,
|
|
263
|
+
0xDD,
|
|
264
|
+
0x74,
|
|
265
|
+
0x1F,
|
|
266
|
+
0x4B,
|
|
267
|
+
0xBD,
|
|
268
|
+
0x8B,
|
|
269
|
+
0x8A,
|
|
270
|
+
0x70,
|
|
271
|
+
0x3E,
|
|
272
|
+
0xB5,
|
|
273
|
+
0x66,
|
|
274
|
+
0x48,
|
|
275
|
+
0x03,
|
|
276
|
+
0xF6,
|
|
277
|
+
0x0E,
|
|
278
|
+
0x61,
|
|
279
|
+
0x35,
|
|
280
|
+
0x57,
|
|
281
|
+
0xB9,
|
|
282
|
+
0x86,
|
|
283
|
+
0xC1,
|
|
284
|
+
0x1D,
|
|
285
|
+
0x9E,
|
|
286
|
+
0xE1,
|
|
287
|
+
0xF8,
|
|
288
|
+
0x98,
|
|
289
|
+
0x11,
|
|
290
|
+
0x69,
|
|
291
|
+
0xD9,
|
|
292
|
+
0x8E,
|
|
293
|
+
0x94,
|
|
294
|
+
0x9B,
|
|
295
|
+
0x1E,
|
|
296
|
+
0x87,
|
|
297
|
+
0xE9,
|
|
298
|
+
0xCE,
|
|
299
|
+
0x55,
|
|
300
|
+
0x28,
|
|
301
|
+
0xDF,
|
|
302
|
+
0x8C,
|
|
303
|
+
0xA1,
|
|
304
|
+
0x89,
|
|
305
|
+
0x0D,
|
|
306
|
+
0xBF,
|
|
307
|
+
0xE6,
|
|
308
|
+
0x42,
|
|
309
|
+
0x68,
|
|
310
|
+
0x41,
|
|
311
|
+
0x99,
|
|
312
|
+
0x2D,
|
|
313
|
+
0x0F,
|
|
314
|
+
0xB0,
|
|
315
|
+
0x54,
|
|
316
|
+
0xBB,
|
|
317
|
+
0x16,
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@dataclass
|
|
322
|
+
class PowerTrace:
|
|
323
|
+
"""Power consumption trace for side-channel analysis.
|
|
324
|
+
|
|
325
|
+
Attributes:
|
|
326
|
+
timestamp: Time points for each power measurement (seconds or sample index).
|
|
327
|
+
power: Power consumption measurements (arbitrary units, e.g., voltage).
|
|
328
|
+
plaintext: Input plaintext for encryption (16 bytes for AES-128).
|
|
329
|
+
ciphertext: Output ciphertext from encryption (optional).
|
|
330
|
+
metadata: Additional trace information (device ID, temperature, etc.).
|
|
331
|
+
|
|
332
|
+
Example:
|
|
333
|
+
>>> import numpy as np
|
|
334
|
+
>>> trace = PowerTrace(
|
|
335
|
+
... timestamp=np.linspace(0, 1e-6, 1000), # 1 microsecond
|
|
336
|
+
... power=np.random.randn(1000),
|
|
337
|
+
... plaintext=bytes(range(16)),
|
|
338
|
+
... metadata={"device": "STM32", "temperature": 25.0}
|
|
339
|
+
... )
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
timestamp: NDArray[np.float64]
|
|
343
|
+
power: NDArray[np.float64]
|
|
344
|
+
plaintext: bytes | None = None
|
|
345
|
+
ciphertext: bytes | None = None
|
|
346
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@dataclass
|
|
350
|
+
class DPAResult:
|
|
351
|
+
"""Result from DPA/CPA/Template attack.
|
|
352
|
+
|
|
353
|
+
Attributes:
|
|
354
|
+
recovered_key: Recovered key bytes (1 byte for single-byte attack).
|
|
355
|
+
key_ranks: Correlation or difference-of-means score for each key guess (0-255).
|
|
356
|
+
Higher values indicate more likely key bytes.
|
|
357
|
+
correlation_traces: Time x key_guess correlation matrix (optional).
|
|
358
|
+
Shape: (256, num_samples) for CPA attacks.
|
|
359
|
+
confidence: Attack confidence score (0.0-1.0).
|
|
360
|
+
Based on separation between best and second-best key guess.
|
|
361
|
+
successful: True if attack confidence exceeds threshold (>0.7).
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
365
|
+
>>> if result.successful:
|
|
366
|
+
... print(f"Key: 0x{result.recovered_key.hex()}")
|
|
367
|
+
... print(f"Confidence: {result.confidence:.2%}")
|
|
368
|
+
... else:
|
|
369
|
+
... print("Attack failed - need more traces")
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
recovered_key: bytes
|
|
373
|
+
key_ranks: NDArray[np.float64]
|
|
374
|
+
correlation_traces: NDArray[np.float64] | None = None
|
|
375
|
+
confidence: float = 0.0
|
|
376
|
+
successful: bool = False
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class DPAAnalyzer:
|
|
380
|
+
"""Differential Power Analysis framework for cryptographic key recovery.
|
|
381
|
+
|
|
382
|
+
This class implements multiple power analysis attack methods for recovering
|
|
383
|
+
secret keys from power consumption traces. Supports DPA, CPA, and Template
|
|
384
|
+
attacks with various leakage models.
|
|
385
|
+
|
|
386
|
+
Attack Types:
|
|
387
|
+
dpa: Classic DPA using difference of means (Kocher et al., 1999)
|
|
388
|
+
cpa: Correlation Power Analysis (Brier et al., 2004)
|
|
389
|
+
template: Template attack with profiling phase (Chari et al., 2003)
|
|
390
|
+
|
|
391
|
+
Leakage Models:
|
|
392
|
+
hamming_weight: Power proportional to number of 1 bits (most common)
|
|
393
|
+
hamming_distance: Power proportional to bit transitions
|
|
394
|
+
identity: Direct intermediate value (linear leakage)
|
|
395
|
+
|
|
396
|
+
Supported Algorithms:
|
|
397
|
+
aes128: AES-128 encryption (16-byte key)
|
|
398
|
+
des: DES encryption (8-byte key, not implemented yet)
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> # Basic CPA attack on AES
|
|
402
|
+
>>> analyzer = DPAAnalyzer(attack_type="cpa", leakage_model="hamming_weight")
|
|
403
|
+
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
404
|
+
>>> # Template attack (profiling + matching)
|
|
405
|
+
>>> analyzer_template = DPAAnalyzer(attack_type="template")
|
|
406
|
+
>>> result = analyzer_template.template_attack(
|
|
407
|
+
... profiling_traces=train_traces,
|
|
408
|
+
... attack_traces=test_traces,
|
|
409
|
+
... target_byte=0
|
|
410
|
+
... )
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
LEAKAGE_MODELS: ClassVar[list[str]] = ["hamming_weight", "hamming_distance", "identity"]
|
|
414
|
+
ATTACK_TYPES: ClassVar[list[str]] = ["dpa", "cpa", "template"]
|
|
415
|
+
|
|
416
|
+
def __init__(
|
|
417
|
+
self,
|
|
418
|
+
attack_type: str = "cpa",
|
|
419
|
+
leakage_model: str = "hamming_weight",
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Initialize DPA analyzer.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
attack_type: Attack method ("dpa", "cpa", "template").
|
|
425
|
+
leakage_model: Power leakage model ("hamming_weight", "hamming_distance",
|
|
426
|
+
"identity").
|
|
427
|
+
|
|
428
|
+
Raises:
|
|
429
|
+
ValueError: If attack_type or leakage_model is invalid.
|
|
430
|
+
|
|
431
|
+
Example:
|
|
432
|
+
>>> analyzer = DPAAnalyzer(attack_type="cpa")
|
|
433
|
+
>>> analyzer = DPAAnalyzer(
|
|
434
|
+
... attack_type="template",
|
|
435
|
+
... leakage_model="hamming_distance"
|
|
436
|
+
... )
|
|
437
|
+
"""
|
|
438
|
+
if attack_type not in self.ATTACK_TYPES:
|
|
439
|
+
msg = f"Invalid attack_type: {attack_type}. Must be one of {self.ATTACK_TYPES}"
|
|
440
|
+
raise ValueError(msg)
|
|
441
|
+
|
|
442
|
+
if leakage_model not in self.LEAKAGE_MODELS:
|
|
443
|
+
msg = f"Invalid leakage_model: {leakage_model}. Must be one of {self.LEAKAGE_MODELS}"
|
|
444
|
+
raise ValueError(msg)
|
|
445
|
+
|
|
446
|
+
self.attack_type = attack_type
|
|
447
|
+
self.leakage_model = leakage_model
|
|
448
|
+
# Templates: key_byte -> (mean vector, covariance matrix)
|
|
449
|
+
self.templates: dict[int, tuple[NDArray[np.float64], NDArray[np.float64]]] = {}
|
|
450
|
+
|
|
451
|
+
def perform_attack(
|
|
452
|
+
self,
|
|
453
|
+
traces: list[PowerTrace],
|
|
454
|
+
target_byte: int = 0,
|
|
455
|
+
algorithm: str = "aes128",
|
|
456
|
+
) -> DPAResult:
|
|
457
|
+
"""Perform power analysis attack to recover key byte.
|
|
458
|
+
|
|
459
|
+
Dispatches to appropriate attack method based on attack_type.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
traces: List of power traces with plaintexts.
|
|
463
|
+
target_byte: Target key byte position (0-15 for AES-128).
|
|
464
|
+
algorithm: Cryptographic algorithm ("aes128" or "des").
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
DPAResult with recovered key and confidence metrics.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
ValueError: If traces is empty, target_byte invalid, or algorithm unsupported.
|
|
471
|
+
|
|
472
|
+
Example:
|
|
473
|
+
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
474
|
+
>>> print(f"Key byte {0}: 0x{result.recovered_key[0]:02X}")
|
|
475
|
+
"""
|
|
476
|
+
if not traces:
|
|
477
|
+
raise ValueError("No traces provided")
|
|
478
|
+
|
|
479
|
+
if target_byte < 0 or target_byte >= 16:
|
|
480
|
+
raise ValueError(f"Invalid target_byte: {target_byte}. Must be 0-15 for AES-128")
|
|
481
|
+
|
|
482
|
+
if algorithm != "aes128":
|
|
483
|
+
raise ValueError(f"Unsupported algorithm: {algorithm}. Only 'aes128' implemented")
|
|
484
|
+
|
|
485
|
+
if self.attack_type == "dpa":
|
|
486
|
+
return self.dpa_attack(traces, target_byte)
|
|
487
|
+
elif self.attack_type == "cpa":
|
|
488
|
+
return self.cpa_attack(traces, target_byte)
|
|
489
|
+
else:
|
|
490
|
+
msg = "Template attack requires separate profiling/attack traces"
|
|
491
|
+
raise ValueError(msg)
|
|
492
|
+
|
|
493
|
+
def dpa_attack(
|
|
494
|
+
self,
|
|
495
|
+
traces: list[PowerTrace],
|
|
496
|
+
target_byte: int,
|
|
497
|
+
) -> DPAResult:
|
|
498
|
+
"""Classic DPA attack using difference of means.
|
|
499
|
+
|
|
500
|
+
Separates traces into two groups based on hypothetical intermediate bit value,
|
|
501
|
+
then computes difference of mean power consumption at each time point.
|
|
502
|
+
|
|
503
|
+
Algorithm:
|
|
504
|
+
1. For each key guess k in [0, 255]:
|
|
505
|
+
a. Compute intermediate value for each trace: v = SBOX[plaintext ^ k]
|
|
506
|
+
b. Partition traces by bit b of v (e.g., LSB)
|
|
507
|
+
c. Compute differential trace: mean(power | b=1) - mean(power | b=0)
|
|
508
|
+
d. Score = max absolute value in differential trace
|
|
509
|
+
2. Return key with highest score
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
traces: List of power traces with plaintexts.
|
|
513
|
+
target_byte: Target key byte position.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
DPAResult with recovered key byte.
|
|
517
|
+
|
|
518
|
+
Example:
|
|
519
|
+
>>> analyzer = DPAAnalyzer(attack_type="dpa")
|
|
520
|
+
>>> result = analyzer.dpa_attack(traces, target_byte=0)
|
|
521
|
+
"""
|
|
522
|
+
if not traces:
|
|
523
|
+
raise ValueError("No traces provided")
|
|
524
|
+
|
|
525
|
+
# Extract plaintexts and power matrix
|
|
526
|
+
plaintexts = [t.plaintext for t in traces if t.plaintext]
|
|
527
|
+
if not plaintexts:
|
|
528
|
+
raise ValueError("No plaintexts in traces")
|
|
529
|
+
|
|
530
|
+
power_matrix = np.array([t.power for t in traces]) # (num_traces, num_samples)
|
|
531
|
+
|
|
532
|
+
# Attack each key guess
|
|
533
|
+
max_diffs = np.zeros(256)
|
|
534
|
+
|
|
535
|
+
for key_guess in range(256):
|
|
536
|
+
# Compute intermediate values and selection bit
|
|
537
|
+
intermediates = []
|
|
538
|
+
for plaintext in plaintexts:
|
|
539
|
+
if plaintext is None or target_byte >= len(plaintext):
|
|
540
|
+
intermediates.append(0)
|
|
541
|
+
continue
|
|
542
|
+
intermediate = self._aes_sbox_output(plaintext[target_byte], key_guess)
|
|
543
|
+
intermediates.append(intermediate)
|
|
544
|
+
|
|
545
|
+
# Use LSB as selection bit
|
|
546
|
+
selection_bits = np.array([v & 1 for v in intermediates])
|
|
547
|
+
|
|
548
|
+
# Partition traces
|
|
549
|
+
group0_traces = power_matrix[selection_bits == 0]
|
|
550
|
+
group1_traces = power_matrix[selection_bits == 1]
|
|
551
|
+
|
|
552
|
+
if len(group0_traces) == 0 or len(group1_traces) == 0:
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
# Compute differential trace
|
|
556
|
+
mean0 = np.mean(group0_traces, axis=0)
|
|
557
|
+
mean1 = np.mean(group1_traces, axis=0)
|
|
558
|
+
diff = mean1 - mean0
|
|
559
|
+
|
|
560
|
+
# Score is maximum absolute difference
|
|
561
|
+
max_diffs[key_guess] = np.max(np.abs(diff))
|
|
562
|
+
|
|
563
|
+
# Find best key guess
|
|
564
|
+
recovered_key_byte = int(np.argmax(max_diffs))
|
|
565
|
+
max_score = max_diffs[recovered_key_byte]
|
|
566
|
+
|
|
567
|
+
# Calculate confidence (separation from second-best)
|
|
568
|
+
sorted_scores = np.sort(max_diffs)
|
|
569
|
+
if sorted_scores[-2] > 0:
|
|
570
|
+
confidence = 1.0 - (sorted_scores[-2] / max_score)
|
|
571
|
+
else:
|
|
572
|
+
confidence = 1.0
|
|
573
|
+
|
|
574
|
+
successful = confidence > 0.7
|
|
575
|
+
|
|
576
|
+
return DPAResult(
|
|
577
|
+
recovered_key=bytes([recovered_key_byte]),
|
|
578
|
+
key_ranks=max_diffs,
|
|
579
|
+
correlation_traces=None,
|
|
580
|
+
confidence=float(confidence),
|
|
581
|
+
successful=successful,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def cpa_attack(
|
|
585
|
+
self,
|
|
586
|
+
traces: list[PowerTrace],
|
|
587
|
+
target_byte: int,
|
|
588
|
+
) -> DPAResult:
|
|
589
|
+
"""Correlation Power Analysis (CPA) attack.
|
|
590
|
+
|
|
591
|
+
Computes Pearson correlation between hypothetical power consumption (based on
|
|
592
|
+
leakage model) and measured power at each time point. Key with highest
|
|
593
|
+
correlation is most likely correct.
|
|
594
|
+
|
|
595
|
+
Algorithm:
|
|
596
|
+
1. For each key guess k in [0, 255]:
|
|
597
|
+
a. Compute hypothetical power: h[i] = LeakageModel(SBOX[plaintext[i] ^ k])
|
|
598
|
+
b. For each time point t:
|
|
599
|
+
- Compute correlation: corr(h, power[:, t])
|
|
600
|
+
c. Score = max |correlation| across all time points
|
|
601
|
+
2. Return key with highest score
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
traces: List of power traces with plaintexts.
|
|
605
|
+
target_byte: Target key byte position.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
DPAResult with recovered key byte and correlation traces.
|
|
609
|
+
|
|
610
|
+
Example:
|
|
611
|
+
>>> analyzer = DPAAnalyzer(attack_type="cpa", leakage_model="hamming_weight")
|
|
612
|
+
>>> result = analyzer.cpa_attack(traces, target_byte=0)
|
|
613
|
+
>>> print(f"Max correlation: {result.confidence:.3f}")
|
|
614
|
+
"""
|
|
615
|
+
if not traces:
|
|
616
|
+
raise ValueError("No traces provided")
|
|
617
|
+
|
|
618
|
+
# Extract plaintexts and power matrix
|
|
619
|
+
plaintexts = [t.plaintext for t in traces if t.plaintext]
|
|
620
|
+
if not plaintexts:
|
|
621
|
+
raise ValueError("No plaintexts in traces")
|
|
622
|
+
|
|
623
|
+
power_matrix = np.array([t.power for t in traces]) # (num_traces, num_samples)
|
|
624
|
+
num_samples = power_matrix.shape[1]
|
|
625
|
+
|
|
626
|
+
# Attack each key guess
|
|
627
|
+
correlation_traces = np.zeros((256, num_samples))
|
|
628
|
+
|
|
629
|
+
for key_guess in range(256):
|
|
630
|
+
# Calculate hypothetical power consumption
|
|
631
|
+
hypothetical = self._calculate_hypothetical_power(
|
|
632
|
+
plaintexts,
|
|
633
|
+
key_guess,
|
|
634
|
+
target_byte,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
# Calculate correlation at each time point
|
|
638
|
+
for sample_idx in range(num_samples):
|
|
639
|
+
measured = power_matrix[:, sample_idx]
|
|
640
|
+
# Use numpy's correlation coefficient
|
|
641
|
+
corr_matrix = np.corrcoef(hypothetical, measured)
|
|
642
|
+
correlation = corr_matrix[0, 1] if corr_matrix.shape == (2, 2) else 0.0
|
|
643
|
+
# Handle NaN from constant signals
|
|
644
|
+
if np.isnan(correlation):
|
|
645
|
+
correlation = 0.0
|
|
646
|
+
correlation_traces[key_guess, sample_idx] = abs(correlation)
|
|
647
|
+
|
|
648
|
+
# Find key guess with maximum correlation
|
|
649
|
+
max_correlations = np.max(correlation_traces, axis=1)
|
|
650
|
+
recovered_key_byte = int(np.argmax(max_correlations))
|
|
651
|
+
max_correlation = max_correlations[recovered_key_byte]
|
|
652
|
+
|
|
653
|
+
# Calculate confidence (separation from second-best)
|
|
654
|
+
sorted_corrs = np.sort(max_correlations)
|
|
655
|
+
if sorted_corrs[-2] > 0:
|
|
656
|
+
confidence = 1.0 - (sorted_corrs[-2] / max_correlation)
|
|
657
|
+
else:
|
|
658
|
+
confidence = 1.0
|
|
659
|
+
|
|
660
|
+
# Check if attack successful (correlation > threshold)
|
|
661
|
+
successful = max_correlation > 0.7
|
|
662
|
+
|
|
663
|
+
return DPAResult(
|
|
664
|
+
recovered_key=bytes([recovered_key_byte]),
|
|
665
|
+
key_ranks=max_correlations,
|
|
666
|
+
correlation_traces=correlation_traces,
|
|
667
|
+
confidence=float(confidence),
|
|
668
|
+
successful=successful,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def template_attack(
|
|
672
|
+
self,
|
|
673
|
+
profiling_traces: list[PowerTrace],
|
|
674
|
+
attack_traces: list[PowerTrace],
|
|
675
|
+
target_byte: int,
|
|
676
|
+
) -> DPAResult:
|
|
677
|
+
"""Template attack with profiling and matching phases.
|
|
678
|
+
|
|
679
|
+
Phase 1 (Profiling): Build templates (mean, covariance) for each key byte
|
|
680
|
+
using traces from controlled device with known key.
|
|
681
|
+
Phase 2 (Matching): Match attack traces to templates using multivariate
|
|
682
|
+
Gaussian probability.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
profiling_traces: Traces from device with known key for template building.
|
|
686
|
+
attack_traces: Traces from target device with unknown key.
|
|
687
|
+
target_byte: Target key byte position.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
DPAResult with recovered key byte.
|
|
691
|
+
|
|
692
|
+
Raises:
|
|
693
|
+
ValueError: If profiling or attack traces are empty.
|
|
694
|
+
|
|
695
|
+
Example:
|
|
696
|
+
>>> analyzer = DPAAnalyzer(attack_type="template")
|
|
697
|
+
>>> # Build templates with known key
|
|
698
|
+
>>> result = analyzer.template_attack(
|
|
699
|
+
... profiling_traces=train_traces,
|
|
700
|
+
... attack_traces=test_traces,
|
|
701
|
+
... target_byte=0
|
|
702
|
+
... )
|
|
703
|
+
"""
|
|
704
|
+
if not profiling_traces or not attack_traces:
|
|
705
|
+
raise ValueError("Both profiling and attack traces required")
|
|
706
|
+
|
|
707
|
+
# Phase 1: Build templates from profiling traces
|
|
708
|
+
self._build_templates(profiling_traces, target_byte)
|
|
709
|
+
|
|
710
|
+
# Phase 2: Match attack traces to templates
|
|
711
|
+
power_matrix = np.array([t.power for t in attack_traces])
|
|
712
|
+
num_samples = power_matrix.shape[1]
|
|
713
|
+
|
|
714
|
+
# Calculate probability for each key guess
|
|
715
|
+
probabilities = np.zeros(256)
|
|
716
|
+
|
|
717
|
+
for key_guess in range(256):
|
|
718
|
+
if key_guess not in self.templates:
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
mean, cov = self.templates[key_guess]
|
|
722
|
+
|
|
723
|
+
# Use only points of interest (reduce dimensionality)
|
|
724
|
+
# For simplicity, use first min(100, num_samples) points
|
|
725
|
+
poi_count = min(100, num_samples)
|
|
726
|
+
mean_poi = mean[:poi_count]
|
|
727
|
+
cov_poi = cov[:poi_count, :poi_count]
|
|
728
|
+
|
|
729
|
+
# Add small regularization to covariance
|
|
730
|
+
cov_poi = cov_poi + np.eye(poi_count) * 1e-6
|
|
731
|
+
|
|
732
|
+
# Calculate log probability for each trace
|
|
733
|
+
log_prob = 0.0
|
|
734
|
+
for trace in power_matrix:
|
|
735
|
+
trace_poi = trace[:poi_count]
|
|
736
|
+
try:
|
|
737
|
+
# Multivariate Gaussian log probability
|
|
738
|
+
diff = trace_poi - mean_poi
|
|
739
|
+
inv_cov = np.linalg.inv(cov_poi)
|
|
740
|
+
log_prob += -0.5 * (diff @ inv_cov @ diff)
|
|
741
|
+
except np.linalg.LinAlgError:
|
|
742
|
+
# Singular covariance matrix
|
|
743
|
+
log_prob += -1e10
|
|
744
|
+
|
|
745
|
+
probabilities[key_guess] = log_prob
|
|
746
|
+
|
|
747
|
+
# Find best key guess
|
|
748
|
+
recovered_key_byte = int(np.argmax(probabilities))
|
|
749
|
+
max_prob = probabilities[recovered_key_byte]
|
|
750
|
+
|
|
751
|
+
# Calculate confidence
|
|
752
|
+
sorted_probs = np.sort(probabilities)
|
|
753
|
+
if sorted_probs[-2] != -np.inf and max_prob != -np.inf and max_prob != 0:
|
|
754
|
+
# Use absolute difference for log probabilities (negative values)
|
|
755
|
+
confidence = abs(max_prob - sorted_probs[-2]) / (abs(max_prob) + 1e-10)
|
|
756
|
+
confidence = min(confidence, 1.0)
|
|
757
|
+
else:
|
|
758
|
+
confidence = 0.0
|
|
759
|
+
|
|
760
|
+
successful = confidence > 0.7
|
|
761
|
+
|
|
762
|
+
return DPAResult(
|
|
763
|
+
recovered_key=bytes([recovered_key_byte]),
|
|
764
|
+
key_ranks=probabilities,
|
|
765
|
+
correlation_traces=None,
|
|
766
|
+
confidence=float(confidence),
|
|
767
|
+
successful=successful,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
def _build_templates(
|
|
771
|
+
self,
|
|
772
|
+
traces: list[PowerTrace],
|
|
773
|
+
target_byte: int,
|
|
774
|
+
) -> None:
|
|
775
|
+
"""Build templates from profiling traces with known key.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
traces: Profiling traces with known plaintexts and ciphertexts.
|
|
779
|
+
target_byte: Target key byte position.
|
|
780
|
+
"""
|
|
781
|
+
# Group traces by intermediate value (assuming key byte = 0 for profiling)
|
|
782
|
+
# In real scenario, you'd know the profiling key
|
|
783
|
+
profiling_key_byte = 0 # Known key for profiling device
|
|
784
|
+
|
|
785
|
+
groups: dict[int, list[NDArray[np.float64]]] = {}
|
|
786
|
+
|
|
787
|
+
for trace in traces:
|
|
788
|
+
if trace.plaintext is None or target_byte >= len(trace.plaintext):
|
|
789
|
+
continue
|
|
790
|
+
|
|
791
|
+
intermediate = self._aes_sbox_output(
|
|
792
|
+
trace.plaintext[target_byte],
|
|
793
|
+
profiling_key_byte,
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if intermediate not in groups:
|
|
797
|
+
groups[intermediate] = []
|
|
798
|
+
groups[intermediate].append(trace.power)
|
|
799
|
+
|
|
800
|
+
# Build template for each intermediate value
|
|
801
|
+
for intermediate, power_traces in groups.items():
|
|
802
|
+
if len(power_traces) < 2:
|
|
803
|
+
continue
|
|
804
|
+
|
|
805
|
+
power_array = np.array(power_traces)
|
|
806
|
+
mean = np.mean(power_array, axis=0)
|
|
807
|
+
cov = np.cov(power_array.T)
|
|
808
|
+
|
|
809
|
+
self.templates[intermediate] = (mean, cov)
|
|
810
|
+
|
|
811
|
+
def _hamming_weight(self, value: int) -> int:
|
|
812
|
+
"""Calculate Hamming weight (population count).
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
value: Integer value (0-255).
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
Number of 1 bits in binary representation (0-8).
|
|
819
|
+
|
|
820
|
+
Example:
|
|
821
|
+
>>> analyzer._hamming_weight(0x0F) # 0b00001111
|
|
822
|
+
4
|
|
823
|
+
>>> analyzer._hamming_weight(0xFF) # 0b11111111
|
|
824
|
+
8
|
|
825
|
+
"""
|
|
826
|
+
count = 0
|
|
827
|
+
while value:
|
|
828
|
+
count += value & 1
|
|
829
|
+
value >>= 1
|
|
830
|
+
return count
|
|
831
|
+
|
|
832
|
+
def _hamming_distance(self, value1: int, value2: int) -> int:
|
|
833
|
+
"""Calculate Hamming distance between two values.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
value1: First integer value (0-255).
|
|
837
|
+
value2: Second integer value (0-255).
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
Number of differing bits (0-8).
|
|
841
|
+
|
|
842
|
+
Example:
|
|
843
|
+
>>> analyzer._hamming_distance(0x00, 0xFF)
|
|
844
|
+
8
|
|
845
|
+
>>> analyzer._hamming_distance(0x0F, 0x0E) # differ in 1 bit
|
|
846
|
+
1
|
|
847
|
+
"""
|
|
848
|
+
return self._hamming_weight(value1 ^ value2)
|
|
849
|
+
|
|
850
|
+
def _aes_sbox_output(self, plaintext_byte: int, key_guess: int) -> int:
|
|
851
|
+
"""Calculate AES S-box output for given plaintext and key guess.
|
|
852
|
+
|
|
853
|
+
Args:
|
|
854
|
+
plaintext_byte: Single plaintext byte (0-255).
|
|
855
|
+
key_guess: Key byte guess (0-255).
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
S-box output (0-255).
|
|
859
|
+
|
|
860
|
+
Example:
|
|
861
|
+
>>> analyzer._aes_sbox_output(0x00, 0x00)
|
|
862
|
+
99 # SBOX[0x00] = 0x63
|
|
863
|
+
"""
|
|
864
|
+
xored = plaintext_byte ^ key_guess
|
|
865
|
+
return AES_SBOX[xored]
|
|
866
|
+
|
|
867
|
+
def _calculate_hypothetical_power(
|
|
868
|
+
self,
|
|
869
|
+
plaintexts: Sequence[bytes | None],
|
|
870
|
+
key_guess: int,
|
|
871
|
+
target_byte: int,
|
|
872
|
+
) -> NDArray[np.float64]:
|
|
873
|
+
"""Calculate hypothetical power consumption for key guess.
|
|
874
|
+
|
|
875
|
+
Uses configured leakage model to convert intermediate values to
|
|
876
|
+
hypothetical power consumption.
|
|
877
|
+
|
|
878
|
+
Args:
|
|
879
|
+
plaintexts: List of plaintext bytes.
|
|
880
|
+
key_guess: Key byte guess (0-255).
|
|
881
|
+
target_byte: Target byte position.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Array of hypothetical power values.
|
|
885
|
+
|
|
886
|
+
Example:
|
|
887
|
+
>>> hyp_power = analyzer._calculate_hypothetical_power(
|
|
888
|
+
... plaintexts=[b'\\x00\\x01\\x02...'],
|
|
889
|
+
... key_guess=0x42,
|
|
890
|
+
... target_byte=0
|
|
891
|
+
... )
|
|
892
|
+
"""
|
|
893
|
+
hypothetical = []
|
|
894
|
+
|
|
895
|
+
for plaintext in plaintexts:
|
|
896
|
+
if plaintext is None or target_byte >= len(plaintext):
|
|
897
|
+
hypothetical.append(0.0)
|
|
898
|
+
continue
|
|
899
|
+
|
|
900
|
+
# Calculate intermediate value (AES S-box output)
|
|
901
|
+
intermediate = self._aes_sbox_output(plaintext[target_byte], key_guess)
|
|
902
|
+
|
|
903
|
+
# Apply leakage model
|
|
904
|
+
if self.leakage_model == "hamming_weight":
|
|
905
|
+
power = float(self._hamming_weight(intermediate))
|
|
906
|
+
elif self.leakage_model == "hamming_distance":
|
|
907
|
+
# Hamming distance from plaintext to intermediate
|
|
908
|
+
power = float(self._hamming_distance(plaintext[target_byte], intermediate))
|
|
909
|
+
else: # identity
|
|
910
|
+
power = float(intermediate)
|
|
911
|
+
|
|
912
|
+
hypothetical.append(power)
|
|
913
|
+
|
|
914
|
+
return np.array(hypothetical)
|
|
915
|
+
|
|
916
|
+
def visualize_attack(
|
|
917
|
+
self,
|
|
918
|
+
result: DPAResult,
|
|
919
|
+
output_path: Path,
|
|
920
|
+
) -> None:
|
|
921
|
+
"""Visualize CPA attack results with correlation traces and key rankings.
|
|
922
|
+
|
|
923
|
+
Creates two-panel plot:
|
|
924
|
+
- Top: Correlation traces for all key guesses (highlighted for recovered key)
|
|
925
|
+
- Bottom: Bar chart of maximum correlation per key guess
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
result: DPAResult from CPA attack.
|
|
929
|
+
output_path: Path to save plot image (PNG format).
|
|
930
|
+
|
|
931
|
+
Raises:
|
|
932
|
+
ValueError: If correlation_traces is None (not a CPA attack).
|
|
933
|
+
ImportError: If matplotlib is not installed.
|
|
934
|
+
|
|
935
|
+
Example:
|
|
936
|
+
>>> result = analyzer.cpa_attack(traces, target_byte=0)
|
|
937
|
+
>>> analyzer.visualize_attack(result, Path("attack_plot.png"))
|
|
938
|
+
"""
|
|
939
|
+
if result.correlation_traces is None:
|
|
940
|
+
raise ValueError("Visualization requires correlation_traces (use CPA attack)")
|
|
941
|
+
|
|
942
|
+
try:
|
|
943
|
+
import matplotlib.pyplot as plt
|
|
944
|
+
except ImportError as e:
|
|
945
|
+
msg = "matplotlib required for visualization"
|
|
946
|
+
raise ImportError(msg) from e
|
|
947
|
+
|
|
948
|
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
|
|
949
|
+
|
|
950
|
+
# Plot 1: Correlation traces for all key guesses
|
|
951
|
+
for key_guess in range(256):
|
|
952
|
+
ax1.plot(
|
|
953
|
+
result.correlation_traces[key_guess],
|
|
954
|
+
alpha=0.1,
|
|
955
|
+
color="blue",
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
# Highlight correct key
|
|
959
|
+
recovered = result.recovered_key[0]
|
|
960
|
+
ax1.plot(
|
|
961
|
+
result.correlation_traces[recovered],
|
|
962
|
+
color="red",
|
|
963
|
+
linewidth=2,
|
|
964
|
+
label=f"Recovered key: 0x{recovered:02X}",
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
ax1.set_xlabel("Sample point")
|
|
968
|
+
ax1.set_ylabel("|Correlation|")
|
|
969
|
+
ax1.set_title("Correlation traces for all key guesses")
|
|
970
|
+
ax1.legend()
|
|
971
|
+
ax1.grid(True, alpha=0.3)
|
|
972
|
+
|
|
973
|
+
# Plot 2: Key ranking (max correlation per key)
|
|
974
|
+
ax2.bar(range(256), result.key_ranks, color="blue", alpha=0.6)
|
|
975
|
+
ax2.axvline(
|
|
976
|
+
result.recovered_key[0],
|
|
977
|
+
color="red",
|
|
978
|
+
linestyle="--",
|
|
979
|
+
linewidth=2,
|
|
980
|
+
label="Recovered key",
|
|
981
|
+
)
|
|
982
|
+
ax2.set_xlabel("Key guess")
|
|
983
|
+
ax2.set_ylabel("Max |Correlation|")
|
|
984
|
+
ax2.set_title("Key ranking")
|
|
985
|
+
ax2.legend()
|
|
986
|
+
ax2.grid(True, alpha=0.3)
|
|
987
|
+
|
|
988
|
+
plt.tight_layout()
|
|
989
|
+
plt.savefig(output_path, dpi=300, bbox_inches="tight")
|
|
990
|
+
plt.close()
|
|
991
|
+
|
|
992
|
+
logger.info(f"Attack visualization saved to {output_path}")
|
|
993
|
+
|
|
994
|
+
def export_results(
|
|
995
|
+
self,
|
|
996
|
+
result: DPAResult,
|
|
997
|
+
output_path: Path,
|
|
998
|
+
) -> None:
|
|
999
|
+
"""Export attack results to JSON file.
|
|
1000
|
+
|
|
1001
|
+
Args:
|
|
1002
|
+
result: DPAResult from attack.
|
|
1003
|
+
output_path: Path to save JSON file.
|
|
1004
|
+
|
|
1005
|
+
Example:
|
|
1006
|
+
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
1007
|
+
>>> analyzer.export_results(result, Path("attack_results.json"))
|
|
1008
|
+
"""
|
|
1009
|
+
data = {
|
|
1010
|
+
"recovered_key": result.recovered_key.hex(),
|
|
1011
|
+
"confidence": result.confidence,
|
|
1012
|
+
"successful": result.successful,
|
|
1013
|
+
"key_ranks": result.key_ranks.tolist(),
|
|
1014
|
+
"attack_type": self.attack_type,
|
|
1015
|
+
"leakage_model": self.leakage_model,
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if result.correlation_traces is not None:
|
|
1019
|
+
# Only export max correlations to reduce file size
|
|
1020
|
+
data["max_correlations"] = np.max(result.correlation_traces, axis=1).tolist()
|
|
1021
|
+
|
|
1022
|
+
with open(output_path, "w") as f:
|
|
1023
|
+
json.dump(data, f, indent=2)
|
|
1024
|
+
|
|
1025
|
+
logger.info(f"Attack results exported to {output_path}")
|