oscura 0.8.0__py3-none-any.whl → 0.10.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.
Files changed (151) hide show
  1. oscura/__init__.py +19 -19
  2. oscura/analyzers/__init__.py +2 -0
  3. oscura/analyzers/digital/extraction.py +2 -3
  4. oscura/analyzers/digital/quality.py +1 -1
  5. oscura/analyzers/digital/timing.py +1 -1
  6. oscura/analyzers/patterns/__init__.py +66 -0
  7. oscura/analyzers/power/basic.py +3 -3
  8. oscura/analyzers/power/soa.py +1 -1
  9. oscura/analyzers/power/switching.py +3 -3
  10. oscura/analyzers/signal_classification.py +529 -0
  11. oscura/analyzers/signal_integrity/sparams.py +3 -3
  12. oscura/analyzers/statistics/basic.py +10 -7
  13. oscura/analyzers/validation.py +1 -1
  14. oscura/analyzers/waveform/measurements.py +200 -156
  15. oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
  16. oscura/analyzers/waveform/spectral.py +164 -73
  17. oscura/api/dsl/commands.py +15 -6
  18. oscura/api/server/templates/base.html +137 -146
  19. oscura/api/server/templates/export.html +84 -110
  20. oscura/api/server/templates/home.html +248 -267
  21. oscura/api/server/templates/protocols.html +44 -48
  22. oscura/api/server/templates/reports.html +27 -35
  23. oscura/api/server/templates/session_detail.html +68 -78
  24. oscura/api/server/templates/sessions.html +62 -72
  25. oscura/api/server/templates/waveforms.html +54 -64
  26. oscura/automotive/__init__.py +1 -1
  27. oscura/automotive/can/session.py +1 -1
  28. oscura/automotive/dbc/generator.py +638 -23
  29. oscura/automotive/uds/decoder.py +99 -6
  30. oscura/cli/analyze.py +8 -2
  31. oscura/cli/batch.py +36 -5
  32. oscura/cli/characterize.py +18 -4
  33. oscura/cli/export.py +47 -5
  34. oscura/cli/main.py +2 -0
  35. oscura/cli/onboarding/wizard.py +10 -6
  36. oscura/cli/pipeline.py +585 -0
  37. oscura/cli/visualize.py +6 -4
  38. oscura/convenience.py +400 -32
  39. oscura/core/measurement_result.py +286 -0
  40. oscura/core/progress.py +1 -1
  41. oscura/core/types.py +232 -239
  42. oscura/correlation/multi_protocol.py +1 -1
  43. oscura/export/legacy/__init__.py +11 -0
  44. oscura/export/legacy/wav.py +75 -0
  45. oscura/exporters/__init__.py +19 -0
  46. oscura/exporters/wireshark.py +809 -0
  47. oscura/hardware/acquisition/file.py +5 -19
  48. oscura/hardware/acquisition/saleae.py +10 -10
  49. oscura/hardware/acquisition/socketcan.py +4 -6
  50. oscura/hardware/acquisition/synthetic.py +1 -5
  51. oscura/hardware/acquisition/visa.py +6 -6
  52. oscura/hardware/security/side_channel_detector.py +5 -508
  53. oscura/inference/message_format.py +686 -1
  54. oscura/jupyter/display.py +2 -2
  55. oscura/jupyter/magic.py +3 -3
  56. oscura/loaders/__init__.py +17 -12
  57. oscura/loaders/binary.py +1 -1
  58. oscura/loaders/chipwhisperer.py +1 -2
  59. oscura/loaders/configurable.py +1 -1
  60. oscura/loaders/csv_loader.py +2 -2
  61. oscura/loaders/hdf5_loader.py +1 -1
  62. oscura/loaders/lazy.py +6 -1
  63. oscura/loaders/mmap_loader.py +0 -1
  64. oscura/loaders/numpy_loader.py +8 -7
  65. oscura/loaders/preprocessing.py +3 -5
  66. oscura/loaders/rigol.py +21 -7
  67. oscura/loaders/sigrok.py +2 -5
  68. oscura/loaders/tdms.py +3 -2
  69. oscura/loaders/tektronix.py +38 -32
  70. oscura/loaders/tss.py +20 -27
  71. oscura/loaders/vcd.py +13 -8
  72. oscura/loaders/wav.py +1 -6
  73. oscura/pipeline/__init__.py +76 -0
  74. oscura/pipeline/handlers/__init__.py +165 -0
  75. oscura/pipeline/handlers/analyzers.py +1045 -0
  76. oscura/pipeline/handlers/decoders.py +899 -0
  77. oscura/pipeline/handlers/exporters.py +1103 -0
  78. oscura/pipeline/handlers/filters.py +891 -0
  79. oscura/pipeline/handlers/loaders.py +640 -0
  80. oscura/pipeline/handlers/transforms.py +768 -0
  81. oscura/reporting/formatting/measurements.py +55 -14
  82. oscura/reporting/templates/enhanced/protocol_re.html +504 -503
  83. oscura/side_channel/__init__.py +38 -57
  84. oscura/utils/builders/signal_builder.py +5 -5
  85. oscura/utils/comparison/compare.py +7 -9
  86. oscura/utils/comparison/golden.py +1 -1
  87. oscura/utils/filtering/convenience.py +2 -2
  88. oscura/utils/math/arithmetic.py +38 -62
  89. oscura/utils/math/interpolation.py +20 -20
  90. oscura/utils/pipeline/__init__.py +4 -17
  91. oscura/utils/progressive.py +1 -4
  92. oscura/utils/triggering/edge.py +1 -1
  93. oscura/utils/triggering/pattern.py +2 -2
  94. oscura/utils/triggering/pulse.py +2 -2
  95. oscura/utils/triggering/window.py +3 -3
  96. oscura/validation/hil_testing.py +11 -11
  97. oscura/visualization/__init__.py +46 -284
  98. oscura/visualization/batch.py +72 -433
  99. oscura/visualization/plot.py +542 -53
  100. oscura/visualization/styles.py +184 -318
  101. oscura/workflows/batch/advanced.py +1 -1
  102. oscura/workflows/batch/aggregate.py +7 -8
  103. oscura/workflows/complete_re.py +251 -23
  104. oscura/workflows/digital.py +27 -4
  105. oscura/workflows/multi_trace.py +136 -17
  106. oscura/workflows/waveform.py +11 -6
  107. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
  108. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
  109. oscura/side_channel/dpa.py +0 -1025
  110. oscura/utils/optimization/__init__.py +0 -19
  111. oscura/utils/optimization/parallel.py +0 -443
  112. oscura/utils/optimization/search.py +0 -532
  113. oscura/utils/pipeline/base.py +0 -338
  114. oscura/utils/pipeline/composition.py +0 -248
  115. oscura/utils/pipeline/parallel.py +0 -449
  116. oscura/utils/pipeline/pipeline.py +0 -375
  117. oscura/utils/search/__init__.py +0 -16
  118. oscura/utils/search/anomaly.py +0 -424
  119. oscura/utils/search/context.py +0 -294
  120. oscura/utils/search/pattern.py +0 -288
  121. oscura/utils/storage/__init__.py +0 -61
  122. oscura/utils/storage/database.py +0 -1166
  123. oscura/visualization/accessibility.py +0 -526
  124. oscura/visualization/annotations.py +0 -371
  125. oscura/visualization/axis_scaling.py +0 -305
  126. oscura/visualization/colors.py +0 -451
  127. oscura/visualization/digital.py +0 -436
  128. oscura/visualization/eye.py +0 -571
  129. oscura/visualization/histogram.py +0 -281
  130. oscura/visualization/interactive.py +0 -1035
  131. oscura/visualization/jitter.py +0 -1042
  132. oscura/visualization/keyboard.py +0 -394
  133. oscura/visualization/layout.py +0 -400
  134. oscura/visualization/optimization.py +0 -1079
  135. oscura/visualization/palettes.py +0 -446
  136. oscura/visualization/power.py +0 -508
  137. oscura/visualization/power_extended.py +0 -955
  138. oscura/visualization/presets.py +0 -469
  139. oscura/visualization/protocols.py +0 -1246
  140. oscura/visualization/render.py +0 -223
  141. oscura/visualization/rendering.py +0 -444
  142. oscura/visualization/reverse_engineering.py +0 -838
  143. oscura/visualization/signal_integrity.py +0 -989
  144. oscura/visualization/specialized.py +0 -643
  145. oscura/visualization/spectral.py +0 -1226
  146. oscura/visualization/thumbnails.py +0 -340
  147. oscura/visualization/time_axis.py +0 -351
  148. oscura/visualization/waveform.py +0 -454
  149. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
  150. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
  151. {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1025 +0,0 @@
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}")