oscura 0.3.0__py3-none-any.whl → 0.5.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 (59) hide show
  1. oscura/__init__.py +1 -7
  2. oscura/acquisition/__init__.py +147 -0
  3. oscura/acquisition/file.py +255 -0
  4. oscura/acquisition/hardware.py +186 -0
  5. oscura/acquisition/saleae.py +340 -0
  6. oscura/acquisition/socketcan.py +315 -0
  7. oscura/acquisition/streaming.py +38 -0
  8. oscura/acquisition/synthetic.py +229 -0
  9. oscura/acquisition/visa.py +376 -0
  10. oscura/analyzers/__init__.py +3 -0
  11. oscura/analyzers/digital/__init__.py +48 -0
  12. oscura/analyzers/digital/clock.py +9 -1
  13. oscura/analyzers/digital/edges.py +1 -1
  14. oscura/analyzers/digital/extraction.py +195 -0
  15. oscura/analyzers/digital/ic_database.py +498 -0
  16. oscura/analyzers/digital/timing.py +41 -11
  17. oscura/analyzers/digital/timing_paths.py +339 -0
  18. oscura/analyzers/digital/vintage.py +377 -0
  19. oscura/analyzers/digital/vintage_result.py +148 -0
  20. oscura/analyzers/protocols/__init__.py +22 -1
  21. oscura/analyzers/protocols/parallel_bus.py +449 -0
  22. oscura/analyzers/side_channel/__init__.py +52 -0
  23. oscura/analyzers/side_channel/power.py +690 -0
  24. oscura/analyzers/side_channel/timing.py +369 -0
  25. oscura/analyzers/signal_integrity/sparams.py +1 -1
  26. oscura/automotive/__init__.py +4 -2
  27. oscura/automotive/can/patterns.py +3 -1
  28. oscura/automotive/can/session.py +277 -78
  29. oscura/automotive/can/state_machine.py +5 -2
  30. oscura/builders/__init__.py +9 -11
  31. oscura/builders/signal_builder.py +99 -191
  32. oscura/core/exceptions.py +5 -1
  33. oscura/export/__init__.py +12 -0
  34. oscura/export/wavedrom.py +430 -0
  35. oscura/exporters/json_export.py +47 -0
  36. oscura/exporters/vintage_logic_csv.py +247 -0
  37. oscura/loaders/__init__.py +1 -0
  38. oscura/loaders/chipwhisperer.py +393 -0
  39. oscura/loaders/touchstone.py +1 -1
  40. oscura/reporting/__init__.py +7 -0
  41. oscura/reporting/vintage_logic_report.py +523 -0
  42. oscura/session/session.py +54 -46
  43. oscura/sessions/__init__.py +70 -0
  44. oscura/sessions/base.py +323 -0
  45. oscura/sessions/blackbox.py +640 -0
  46. oscura/sessions/generic.py +189 -0
  47. oscura/utils/autodetect.py +5 -1
  48. oscura/visualization/digital_advanced.py +718 -0
  49. oscura/visualization/figure_manager.py +156 -0
  50. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/METADATA +86 -5
  51. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/RECORD +54 -33
  52. oscura/automotive/dtc/data.json +0 -2763
  53. oscura/schemas/bus_configuration.json +0 -322
  54. oscura/schemas/device_mapping.json +0 -182
  55. oscura/schemas/packet_format.json +0 -418
  56. oscura/schemas/protocol_definition.json +0 -363
  57. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/WHEEL +0 -0
  58. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/entry_points.txt +0 -0
  59. {oscura-0.3.0.dist-info → oscura-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,690 @@
1
+ """Power analysis side-channel attacks.
2
+
3
+ This module implements Differential Power Analysis (DPA) and Correlation Power
4
+ Analysis (CPA) for extracting cryptographic keys from power consumption traces.
5
+
6
+ Example:
7
+ >>> import numpy as np
8
+ >>> from oscura.analyzers.side_channel.power import CPAAnalyzer
9
+ >>>
10
+ >>> # Perform CPA attack on AES
11
+ >>> traces = np.array([...]) # Power traces (n_traces, n_samples)
12
+ >>> plaintexts = np.array([...]) # Known plaintexts
13
+ >>> cpa = CPAAnalyzer(leakage_model="hamming_weight", algorithm="aes_sbox")
14
+ >>> result = cpa.analyze(traces, plaintexts)
15
+ >>> print(f"Best key guess: 0x{result.key_guess:02X}")
16
+
17
+ References:
18
+ Kocher et al. "Differential Power Analysis" (CRYPTO 1999)
19
+ Brier et al. "Correlation Power Analysis with a Leakage Model" (CHES 2004)
20
+ Mangard et al. "Power Analysis Attacks" (Springer 2007)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Callable
26
+ from dataclasses import dataclass
27
+ from enum import Enum
28
+ from typing import TYPE_CHECKING, Any, Literal
29
+
30
+ import numpy as np
31
+
32
+ if TYPE_CHECKING:
33
+ from numpy.typing import NDArray
34
+
35
+ __all__ = [
36
+ "CPAAnalyzer",
37
+ "CPAResult",
38
+ "DPAAnalyzer",
39
+ "DPAResult",
40
+ "LeakageModel",
41
+ "hamming_distance",
42
+ "hamming_weight",
43
+ ]
44
+
45
+
46
+ class LeakageModel(Enum):
47
+ """Power leakage models.
48
+
49
+ Attributes:
50
+ HAMMING_WEIGHT: Count of '1' bits in value.
51
+ HAMMING_DISTANCE: Count of bit flips between states.
52
+ IDENTITY: Raw value (linear leakage).
53
+ """
54
+
55
+ HAMMING_WEIGHT = "hamming_weight"
56
+ HAMMING_DISTANCE = "hamming_distance"
57
+ IDENTITY = "identity"
58
+
59
+
60
+ def hamming_weight(value: int | NDArray[np.integer[Any]]) -> int | NDArray[np.integer[Any]]:
61
+ """Calculate Hamming weight (number of 1 bits).
62
+
63
+ Args:
64
+ value: Integer or array of integers.
65
+
66
+ Returns:
67
+ Hamming weight(s).
68
+
69
+ Example:
70
+ >>> hamming_weight(0x0F)
71
+ 4
72
+ >>> hamming_weight(np.array([0x0F, 0xFF]))
73
+ array([4, 8])
74
+ """
75
+ if isinstance(value, np.ndarray):
76
+ # Vectorized implementation for arrays
77
+ result = np.zeros(value.shape, dtype=np.int32)
78
+ temp_arr = value.astype(np.uint32)
79
+ while np.any(temp_arr):
80
+ result += temp_arr & 1
81
+ temp_arr >>= 1
82
+ return result
83
+ else:
84
+ # Scalar implementation
85
+ count: int = 0
86
+ temp: int = int(value)
87
+ while temp:
88
+ count += temp & 1
89
+ temp >>= 1
90
+ return count
91
+
92
+
93
+ def hamming_distance(
94
+ val1: int | NDArray[np.integer[Any]], val2: int | NDArray[np.integer[Any]]
95
+ ) -> int | NDArray[np.integer[Any]]:
96
+ """Calculate Hamming distance (number of differing bits).
97
+
98
+ Args:
99
+ val1: First value(s).
100
+ val2: Second value(s).
101
+
102
+ Returns:
103
+ Hamming distance(s).
104
+
105
+ Example:
106
+ >>> hamming_distance(0x00, 0xFF)
107
+ 8
108
+ >>> hamming_distance(0x0F, 0xF0)
109
+ 8
110
+ """
111
+ if isinstance(val1, np.ndarray) or isinstance(val2, np.ndarray):
112
+ v1 = np.asarray(val1)
113
+ v2 = np.asarray(val2)
114
+ return hamming_weight(np.bitwise_xor(v1, v2))
115
+ else:
116
+ return hamming_weight(val1 ^ val2)
117
+
118
+
119
+ # AES S-box for cryptographic modeling
120
+ AES_SBOX = np.array(
121
+ [
122
+ 0x63,
123
+ 0x7C,
124
+ 0x77,
125
+ 0x7B,
126
+ 0xF2,
127
+ 0x6B,
128
+ 0x6F,
129
+ 0xC5,
130
+ 0x30,
131
+ 0x01,
132
+ 0x67,
133
+ 0x2B,
134
+ 0xFE,
135
+ 0xD7,
136
+ 0xAB,
137
+ 0x76,
138
+ 0xCA,
139
+ 0x82,
140
+ 0xC9,
141
+ 0x7D,
142
+ 0xFA,
143
+ 0x59,
144
+ 0x47,
145
+ 0xF0,
146
+ 0xAD,
147
+ 0xD4,
148
+ 0xA2,
149
+ 0xAF,
150
+ 0x9C,
151
+ 0xA4,
152
+ 0x72,
153
+ 0xC0,
154
+ 0xB7,
155
+ 0xFD,
156
+ 0x93,
157
+ 0x26,
158
+ 0x36,
159
+ 0x3F,
160
+ 0xF7,
161
+ 0xCC,
162
+ 0x34,
163
+ 0xA5,
164
+ 0xE5,
165
+ 0xF1,
166
+ 0x71,
167
+ 0xD8,
168
+ 0x31,
169
+ 0x15,
170
+ 0x04,
171
+ 0xC7,
172
+ 0x23,
173
+ 0xC3,
174
+ 0x18,
175
+ 0x96,
176
+ 0x05,
177
+ 0x9A,
178
+ 0x07,
179
+ 0x12,
180
+ 0x80,
181
+ 0xE2,
182
+ 0xEB,
183
+ 0x27,
184
+ 0xB2,
185
+ 0x75,
186
+ 0x09,
187
+ 0x83,
188
+ 0x2C,
189
+ 0x1A,
190
+ 0x1B,
191
+ 0x6E,
192
+ 0x5A,
193
+ 0xA0,
194
+ 0x52,
195
+ 0x3B,
196
+ 0xD6,
197
+ 0xB3,
198
+ 0x29,
199
+ 0xE3,
200
+ 0x2F,
201
+ 0x84,
202
+ 0x53,
203
+ 0xD1,
204
+ 0x00,
205
+ 0xED,
206
+ 0x20,
207
+ 0xFC,
208
+ 0xB1,
209
+ 0x5B,
210
+ 0x6A,
211
+ 0xCB,
212
+ 0xBE,
213
+ 0x39,
214
+ 0x4A,
215
+ 0x4C,
216
+ 0x58,
217
+ 0xCF,
218
+ 0xD0,
219
+ 0xEF,
220
+ 0xAA,
221
+ 0xFB,
222
+ 0x43,
223
+ 0x4D,
224
+ 0x33,
225
+ 0x85,
226
+ 0x45,
227
+ 0xF9,
228
+ 0x02,
229
+ 0x7F,
230
+ 0x50,
231
+ 0x3C,
232
+ 0x9F,
233
+ 0xA8,
234
+ 0x51,
235
+ 0xA3,
236
+ 0x40,
237
+ 0x8F,
238
+ 0x92,
239
+ 0x9D,
240
+ 0x38,
241
+ 0xF5,
242
+ 0xBC,
243
+ 0xB6,
244
+ 0xDA,
245
+ 0x21,
246
+ 0x10,
247
+ 0xFF,
248
+ 0xF3,
249
+ 0xD2,
250
+ 0xCD,
251
+ 0x0C,
252
+ 0x13,
253
+ 0xEC,
254
+ 0x5F,
255
+ 0x97,
256
+ 0x44,
257
+ 0x17,
258
+ 0xC4,
259
+ 0xA7,
260
+ 0x7E,
261
+ 0x3D,
262
+ 0x64,
263
+ 0x5D,
264
+ 0x19,
265
+ 0x73,
266
+ 0x60,
267
+ 0x81,
268
+ 0x4F,
269
+ 0xDC,
270
+ 0x22,
271
+ 0x2A,
272
+ 0x90,
273
+ 0x88,
274
+ 0x46,
275
+ 0xEE,
276
+ 0xB8,
277
+ 0x14,
278
+ 0xDE,
279
+ 0x5E,
280
+ 0x0B,
281
+ 0xDB,
282
+ 0xE0,
283
+ 0x32,
284
+ 0x3A,
285
+ 0x0A,
286
+ 0x49,
287
+ 0x06,
288
+ 0x24,
289
+ 0x5C,
290
+ 0xC2,
291
+ 0xD3,
292
+ 0xAC,
293
+ 0x62,
294
+ 0x91,
295
+ 0x95,
296
+ 0xE4,
297
+ 0x79,
298
+ 0xE7,
299
+ 0xC8,
300
+ 0x37,
301
+ 0x6D,
302
+ 0x8D,
303
+ 0xD5,
304
+ 0x4E,
305
+ 0xA9,
306
+ 0x6C,
307
+ 0x56,
308
+ 0xF4,
309
+ 0xEA,
310
+ 0x65,
311
+ 0x7A,
312
+ 0xAE,
313
+ 0x08,
314
+ 0xBA,
315
+ 0x78,
316
+ 0x25,
317
+ 0x2E,
318
+ 0x1C,
319
+ 0xA6,
320
+ 0xB4,
321
+ 0xC6,
322
+ 0xE8,
323
+ 0xDD,
324
+ 0x74,
325
+ 0x1F,
326
+ 0x4B,
327
+ 0xBD,
328
+ 0x8B,
329
+ 0x8A,
330
+ 0x70,
331
+ 0x3E,
332
+ 0xB5,
333
+ 0x66,
334
+ 0x48,
335
+ 0x03,
336
+ 0xF6,
337
+ 0x0E,
338
+ 0x61,
339
+ 0x35,
340
+ 0x57,
341
+ 0xB9,
342
+ 0x86,
343
+ 0xC1,
344
+ 0x1D,
345
+ 0x9E,
346
+ 0xE1,
347
+ 0xF8,
348
+ 0x98,
349
+ 0x11,
350
+ 0x69,
351
+ 0xD9,
352
+ 0x8E,
353
+ 0x94,
354
+ 0x9B,
355
+ 0x1E,
356
+ 0x87,
357
+ 0xE9,
358
+ 0xCE,
359
+ 0x55,
360
+ 0x28,
361
+ 0xDF,
362
+ 0x8C,
363
+ 0xA1,
364
+ 0x89,
365
+ 0x0D,
366
+ 0xBF,
367
+ 0xE6,
368
+ 0x42,
369
+ 0x68,
370
+ 0x41,
371
+ 0x99,
372
+ 0x2D,
373
+ 0x0F,
374
+ 0xB0,
375
+ 0x54,
376
+ 0xBB,
377
+ 0x16,
378
+ ],
379
+ dtype=np.uint8,
380
+ )
381
+
382
+
383
+ @dataclass
384
+ class DPAResult:
385
+ """Result of Differential Power Analysis attack.
386
+
387
+ Attributes:
388
+ key_guess: Most likely key byte value.
389
+ differential_traces: Differential traces for each key hypothesis (256, n_samples).
390
+ max_differential: Maximum differential value achieved.
391
+ key_rank: Ranking of all key hypotheses by differential.
392
+ peak_sample: Sample index where maximum differential occurs.
393
+ """
394
+
395
+ key_guess: int
396
+ differential_traces: NDArray[np.floating[Any]]
397
+ max_differential: float
398
+ key_rank: NDArray[np.integer[Any]]
399
+ peak_sample: int
400
+
401
+
402
+ @dataclass
403
+ class CPAResult:
404
+ """Result of Correlation Power Analysis attack.
405
+
406
+ Attributes:
407
+ key_guess: Most likely key byte value.
408
+ max_correlation: Maximum correlation coefficient achieved.
409
+ correlations: Correlation values for each key hypothesis (256, n_samples).
410
+ key_rank: Ranking of all key hypotheses by correlation.
411
+ peak_sample: Sample index where maximum correlation occurs.
412
+ """
413
+
414
+ key_guess: int
415
+ max_correlation: float
416
+ correlations: NDArray[np.floating[Any]]
417
+ key_rank: NDArray[np.integer[Any]]
418
+ peak_sample: int
419
+
420
+
421
+ class DPAAnalyzer:
422
+ """Differential Power Analysis (DPA) attack implementation.
423
+
424
+ DPA exploits differences in power consumption based on data-dependent
425
+ operations. Traces are partitioned by a selection function and averaged
426
+ to reveal key-dependent differences.
427
+
428
+ Args:
429
+ target_bit: Bit position to target (0-7) for AES S-box output.
430
+ byte_position: Byte position in key (0-15 for AES-128).
431
+
432
+ Example:
433
+ >>> dpa = DPAAnalyzer(target_bit=0, byte_position=0)
434
+ >>> result = dpa.analyze(power_traces, known_plaintexts)
435
+ >>> print(f"Key byte 0: 0x{result.key_guess:02X}")
436
+
437
+ References:
438
+ Kocher et al. "Differential Power Analysis" CRYPTO 1999
439
+ """
440
+
441
+ def __init__(self, target_bit: int = 0, byte_position: int = 0) -> None:
442
+ """Initialize DPA analyzer.
443
+
444
+ Args:
445
+ target_bit: Target bit position (0-7).
446
+ byte_position: Key byte position (0-15).
447
+
448
+ Raises:
449
+ ValueError: If parameters out of range.
450
+ """
451
+ if not 0 <= target_bit <= 7:
452
+ raise ValueError(f"target_bit must be 0-7, got {target_bit}")
453
+ if not 0 <= byte_position <= 15:
454
+ raise ValueError(f"byte_position must be 0-15, got {byte_position}")
455
+
456
+ self.target_bit = target_bit
457
+ self.byte_position = byte_position
458
+
459
+ def _selection_function(self, plaintext_byte: int, key_guess: int) -> int:
460
+ """Selection function: bit value of S-box output.
461
+
462
+ Args:
463
+ plaintext_byte: Input plaintext byte.
464
+ key_guess: Hypothetical key byte.
465
+
466
+ Returns:
467
+ Value of target bit (0 or 1).
468
+ """
469
+ sbox_out = AES_SBOX[plaintext_byte ^ key_guess]
470
+ return int((sbox_out >> self.target_bit) & 1)
471
+
472
+ def analyze(
473
+ self,
474
+ traces: NDArray[np.floating[Any]],
475
+ plaintexts: NDArray[np.integer[Any]],
476
+ ) -> DPAResult:
477
+ """Perform DPA attack to recover key byte.
478
+
479
+ Args:
480
+ traces: Power traces (n_traces, n_samples).
481
+ plaintexts: Known plaintexts (n_traces, 16) or (n_traces,) if single byte.
482
+
483
+ Returns:
484
+ DPAResult with key guess and differential traces.
485
+
486
+ Raises:
487
+ ValueError: If input shapes incompatible.
488
+
489
+ Example:
490
+ >>> traces = np.random.randn(1000, 5000) # 1000 traces, 5000 samples
491
+ >>> plaintexts = np.random.randint(0, 256, (1000, 16), dtype=np.uint8)
492
+ >>> result = dpa.analyze(traces, plaintexts)
493
+ """
494
+ n_traces, n_samples = traces.shape
495
+
496
+ # Extract target byte from plaintexts
497
+ if plaintexts.ndim == 1:
498
+ plaintext_bytes = plaintexts
499
+ elif plaintexts.ndim == 2:
500
+ plaintext_bytes = plaintexts[:, self.byte_position]
501
+ else:
502
+ raise ValueError(f"plaintexts must be 1D or 2D, got shape {plaintexts.shape}")
503
+
504
+ if len(plaintext_bytes) != n_traces:
505
+ raise ValueError(
506
+ f"Number of plaintexts ({len(plaintext_bytes)}) must match traces ({n_traces})"
507
+ )
508
+
509
+ # Calculate differential for each key hypothesis
510
+ differential_traces = np.zeros((256, n_samples), dtype=np.float64)
511
+
512
+ for key_guess in range(256):
513
+ # Partition traces by selection function
514
+ selection_bits = np.array(
515
+ [self._selection_function(pt, key_guess) for pt in plaintext_bytes]
516
+ )
517
+
518
+ set_0 = traces[selection_bits == 0]
519
+ set_1 = traces[selection_bits == 1]
520
+
521
+ if len(set_0) > 0 and len(set_1) > 0:
522
+ # Differential = mean(set_1) - mean(set_0)
523
+ differential_traces[key_guess] = np.mean(set_1, axis=0) - np.mean(set_0, axis=0)
524
+
525
+ # Find key with maximum differential
526
+ max_differentials = np.max(np.abs(differential_traces), axis=1)
527
+ key_rank = np.argsort(max_differentials)[::-1] # Descending order
528
+ key_guess = key_rank[0]
529
+ max_differential = max_differentials[key_guess]
530
+ peak_sample = int(np.argmax(np.abs(differential_traces[key_guess])))
531
+
532
+ return DPAResult(
533
+ key_guess=int(key_guess),
534
+ differential_traces=differential_traces,
535
+ max_differential=float(max_differential),
536
+ key_rank=key_rank,
537
+ peak_sample=peak_sample,
538
+ )
539
+
540
+
541
+ class CPAAnalyzer:
542
+ """Correlation Power Analysis (CPA) attack implementation.
543
+
544
+ CPA uses statistical correlation between power consumption and
545
+ intermediate values predicted by a leakage model.
546
+
547
+ Args:
548
+ leakage_model: Leakage model ("hamming_weight", "hamming_distance", "identity").
549
+ algorithm: Target algorithm ("aes_sbox", "des", "custom").
550
+ byte_position: Key byte position to attack (0-15).
551
+
552
+ Example:
553
+ >>> cpa = CPAAnalyzer(leakage_model="hamming_weight", algorithm="aes_sbox")
554
+ >>> result = cpa.analyze(power_traces, known_plaintexts)
555
+ >>> print(f"Correlation: {result.max_correlation:.4f}")
556
+
557
+ References:
558
+ Brier et al. "Correlation Power Analysis" CHES 2004
559
+ """
560
+
561
+ def __init__(
562
+ self,
563
+ leakage_model: Literal["hamming_weight", "hamming_distance", "identity"] = "hamming_weight",
564
+ algorithm: Literal["aes_sbox", "des", "custom"] = "aes_sbox",
565
+ byte_position: int = 0,
566
+ ) -> None:
567
+ """Initialize CPA analyzer.
568
+
569
+ Args:
570
+ leakage_model: Power leakage model.
571
+ algorithm: Target cryptographic algorithm.
572
+ byte_position: Target key byte position.
573
+
574
+ Raises:
575
+ ValueError: If parameters invalid.
576
+ """
577
+ valid_models = ["hamming_weight", "hamming_distance", "identity"]
578
+ if leakage_model not in valid_models:
579
+ raise ValueError(f"leakage_model must be one of {valid_models}")
580
+
581
+ if not 0 <= byte_position <= 15:
582
+ raise ValueError(f"byte_position must be 0-15, got {byte_position}")
583
+
584
+ self.leakage_model = leakage_model
585
+ self.algorithm = algorithm
586
+ self.byte_position = byte_position
587
+
588
+ # Select leakage function
589
+ if leakage_model == "hamming_weight":
590
+ self._leakage_func: Callable[[NDArray[np.integer[Any]]], NDArray[np.integer[Any]]] = (
591
+ hamming_weight # type: ignore[assignment]
592
+ )
593
+ elif leakage_model == "identity":
594
+ self._leakage_func = lambda x: x # Identity function cannot be simplified
595
+ else:
596
+ self._leakage_func = hamming_weight # type: ignore[assignment]
597
+
598
+ def _compute_intermediate(
599
+ self, plaintext_byte: NDArray[np.integer[Any]], key_guess: int
600
+ ) -> NDArray[np.integer[Any]]:
601
+ """Compute intermediate value for key hypothesis.
602
+
603
+ Args:
604
+ plaintext_byte: Plaintext byte values.
605
+ key_guess: Hypothetical key byte.
606
+
607
+ Returns:
608
+ Intermediate values (e.g., S-box output).
609
+ """
610
+ if self.algorithm == "aes_sbox":
611
+ result: NDArray[np.integer[Any]] = AES_SBOX[plaintext_byte ^ key_guess]
612
+ return result
613
+ else:
614
+ # Default: XOR with key
615
+ return plaintext_byte ^ key_guess
616
+
617
+ def analyze(
618
+ self,
619
+ traces: NDArray[np.floating[Any]],
620
+ plaintexts: NDArray[np.integer[Any]],
621
+ ) -> CPAResult:
622
+ """Perform CPA attack to recover key byte.
623
+
624
+ Args:
625
+ traces: Power traces (n_traces, n_samples).
626
+ plaintexts: Known plaintexts (n_traces, 16) or (n_traces,).
627
+
628
+ Returns:
629
+ CPAResult with key guess and correlation matrix.
630
+
631
+ Raises:
632
+ ValueError: If input shapes incompatible.
633
+
634
+ Example:
635
+ >>> traces = np.random.randn(1000, 5000)
636
+ >>> plaintexts = np.random.randint(0, 256, (1000, 16), dtype=np.uint8)
637
+ >>> result = cpa.analyze(traces, plaintexts)
638
+ >>> print(f"Best key: 0x{result.key_guess:02X}")
639
+ """
640
+ n_traces, n_samples = traces.shape
641
+
642
+ # Extract target byte
643
+ if plaintexts.ndim == 1:
644
+ plaintext_bytes = plaintexts
645
+ elif plaintexts.ndim == 2:
646
+ plaintext_bytes = plaintexts[:, self.byte_position]
647
+ else:
648
+ raise ValueError(f"plaintexts must be 1D or 2D, got shape {plaintexts.shape}")
649
+
650
+ if len(plaintext_bytes) != n_traces:
651
+ raise ValueError(
652
+ f"Number of plaintexts ({len(plaintext_bytes)}) must match traces ({n_traces})"
653
+ )
654
+
655
+ # Compute correlations for all key hypotheses
656
+ correlations = np.zeros((256, n_samples), dtype=np.float64)
657
+
658
+ for key_guess in range(256):
659
+ # Compute intermediate values
660
+ intermediates = self._compute_intermediate(plaintext_bytes, key_guess)
661
+
662
+ # Apply leakage model
663
+ hypothetical_power = self._leakage_func(intermediates).astype(np.float64)
664
+
665
+ # Compute Pearson correlation for each sample point
666
+ for sample_idx in range(n_samples):
667
+ trace_sample = traces[:, sample_idx]
668
+
669
+ # Pearson correlation coefficient
670
+ correlations[key_guess, sample_idx] = np.corrcoef(hypothetical_power, trace_sample)[
671
+ 0, 1
672
+ ]
673
+
674
+ # Handle NaN values (can occur with constant traces)
675
+ correlations = np.nan_to_num(correlations, nan=0.0)
676
+
677
+ # Find key with maximum absolute correlation
678
+ max_correlations = np.max(np.abs(correlations), axis=1)
679
+ key_rank = np.argsort(max_correlations)[::-1]
680
+ key_guess = key_rank[0]
681
+ max_correlation = max_correlations[key_guess]
682
+ peak_sample = int(np.argmax(np.abs(correlations[key_guess])))
683
+
684
+ return CPAResult(
685
+ key_guess=int(key_guess),
686
+ max_correlation=float(max_correlation),
687
+ correlations=correlations,
688
+ key_rank=key_rank,
689
+ peak_sample=peak_sample,
690
+ )