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,449 @@
1
+ """Parallel bus protocol decoders for vintage systems.
2
+
3
+ Implements decoders for classic parallel bus protocols:
4
+ - IEEE-488 (GPIB): General Purpose Interface Bus for instruments
5
+ - Centronics: Parallel printer interface
6
+ - ISA: Industry Standard Architecture bus
7
+
8
+ Example:
9
+ >>> from oscura.analyzers.protocols.parallel_bus import decode_gpib, decode_centronics
10
+ >>> frames = decode_gpib(dio_lines, dav, nrfd, ndac, eoi, atn)
11
+ >>> print_data = decode_centronics(data_lines, strobe, busy, ack)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from enum import Enum
18
+ from typing import TYPE_CHECKING
19
+
20
+ import numpy as np
21
+
22
+ if TYPE_CHECKING:
23
+ from numpy.typing import NDArray
24
+
25
+
26
+ # =============================================================================
27
+ # IEEE-488 (GPIB) Protocol Decoder
28
+ # =============================================================================
29
+
30
+
31
+ class GPIBMessageType(Enum):
32
+ """GPIB message types."""
33
+
34
+ DATA = "data" # Data bytes (ATN=0)
35
+ COMMAND = "command" # Command bytes (ATN=1)
36
+ TALK_ADDRESS = "talk_address" # Talk address
37
+ LISTEN_ADDRESS = "listen_address" # Listen address
38
+ SECONDARY_ADDRESS = "secondary_address" # Secondary address
39
+ UNIVERSAL_COMMAND = "universal_command" # Universal command
40
+ ADDRESSED_COMMAND = "addressed_command" # Addressed command
41
+
42
+
43
+ @dataclass
44
+ class GPIBFrame:
45
+ """A decoded GPIB frame.
46
+
47
+ Attributes:
48
+ timestamp: Frame timestamp in seconds.
49
+ data: Data byte value (0-255).
50
+ message_type: Type of GPIB message.
51
+ eoi: End-Or-Identify asserted.
52
+ description: Human-readable description.
53
+ """
54
+
55
+ timestamp: float
56
+ data: int
57
+ message_type: GPIBMessageType
58
+ eoi: bool
59
+ description: str
60
+
61
+
62
+ def decode_gpib(
63
+ dio_lines: list[NDArray[np.bool_]], # DIO1-DIO8 (8 data lines)
64
+ dav: NDArray[np.bool_], # Data Valid
65
+ nrfd: NDArray[np.bool_], # Not Ready For Data
66
+ ndac: NDArray[np.bool_], # Not Data Accepted
67
+ eoi: NDArray[np.bool_], # End Or Identify
68
+ atn: NDArray[np.bool_], # Attention
69
+ sample_rate: float = 1.0,
70
+ ) -> list[GPIBFrame]:
71
+ """Decode IEEE-488 (GPIB) bus transactions.
72
+
73
+ Args:
74
+ dio_lines: List of 8 digital traces for DIO1-DIO8.
75
+ dav: Data Valid signal (active low).
76
+ nrfd: Not Ready For Data signal (active low).
77
+ ndac: Not Data Accepted signal (active low).
78
+ eoi: End Or Identify signal (active low).
79
+ atn: Attention signal (active low).
80
+ sample_rate: Sample rate in Hz.
81
+
82
+ Returns:
83
+ List of GPIBFrame objects.
84
+
85
+ Example:
86
+ >>> frames = decode_gpib(dio, dav, nrfd, ndac, eoi, atn, 1e6)
87
+ >>> for frame in frames:
88
+ ... print(f"{frame.timestamp*1e6:.1f}us: {frame.description}")
89
+ """
90
+ if len(dio_lines) != 8:
91
+ raise ValueError("GPIB requires exactly 8 DIO lines")
92
+
93
+ frames: list[GPIBFrame] = []
94
+ time_base = 1.0 / sample_rate
95
+
96
+ # Combine DIO lines into data bus
97
+ data_bus = np.zeros(len(dio_lines[0]), dtype=np.uint8)
98
+ for i, line in enumerate(dio_lines):
99
+ data_bus |= (line.astype(np.uint8) << i).astype(np.uint8)
100
+
101
+ # Detect DAV falling edges (data valid)
102
+ dav_falling = np.where(np.diff(dav.astype(np.int8)) == -1)[0]
103
+
104
+ for idx in dav_falling:
105
+ # Sample data after falling edge
106
+ sample_idx = idx + 1
107
+ if sample_idx >= len(data_bus):
108
+ continue
109
+
110
+ timestamp = idx * time_base
111
+ data_byte = int(data_bus[sample_idx])
112
+ eoi_active = not eoi[sample_idx] # Active low
113
+ atn_active = not atn[sample_idx] # Active low
114
+
115
+ # Decode message type based on ATN and data
116
+ if atn_active:
117
+ # Check address bytes first (bit patterns for talk/listen)
118
+ if data_byte & 0x40:
119
+ # Talk address (bit 6 set)
120
+ address = data_byte & 0x1F
121
+ msg_type = GPIBMessageType.TALK_ADDRESS
122
+ desc = f"Talk address {address}"
123
+ elif data_byte & 0x20:
124
+ # Listen address (bit 5 set, bit 6 clear)
125
+ address = data_byte & 0x1F
126
+ msg_type = GPIBMessageType.LISTEN_ADDRESS
127
+ desc = f"Listen address {address}"
128
+ elif 0x10 <= data_byte <= 0x1F:
129
+ # Universal commands
130
+ msg_type = GPIBMessageType.UNIVERSAL_COMMAND
131
+ desc = _gpib_universal_command_name(data_byte)
132
+ else:
133
+ # Addressed commands (0x00-0x0F and others)
134
+ msg_type = GPIBMessageType.ADDRESSED_COMMAND
135
+ desc = _gpib_addressed_command_name(data_byte)
136
+ else:
137
+ # Data byte
138
+ msg_type = GPIBMessageType.DATA
139
+ desc = f"Data: 0x{data_byte:02X}"
140
+ if 32 <= data_byte <= 126:
141
+ desc += f" ('{chr(data_byte)}')"
142
+
143
+ if eoi_active:
144
+ desc += " [EOI]"
145
+
146
+ frames.append(
147
+ GPIBFrame(
148
+ timestamp=timestamp,
149
+ data=data_byte,
150
+ message_type=msg_type,
151
+ eoi=eoi_active,
152
+ description=desc,
153
+ )
154
+ )
155
+
156
+ return frames
157
+
158
+
159
+ def _gpib_universal_command_name(cmd: int) -> str:
160
+ """Get name of GPIB universal command."""
161
+ commands = {
162
+ 0x11: "DCL (Device Clear)",
163
+ 0x14: "GET (Group Execute Trigger)",
164
+ 0x15: "GTL (Go To Local)",
165
+ 0x08: "LLO (Local Lockout)",
166
+ 0x01: "SPD (Serial Poll Disable)",
167
+ 0x18: "SPE (Serial Poll Enable)",
168
+ 0x13: "PPU (Parallel Poll Unconfigure)",
169
+ }
170
+ return commands.get(cmd, f"Unknown universal command 0x{cmd:02X}")
171
+
172
+
173
+ def _gpib_addressed_command_name(cmd: int) -> str:
174
+ """Get name of GPIB addressed command."""
175
+ commands = {
176
+ 0x04: "SDC (Selected Device Clear)",
177
+ 0x05: "PPC (Parallel Poll Configure)",
178
+ 0x09: "TCT (Take Control)",
179
+ }
180
+ return commands.get(cmd, f"Unknown addressed command 0x{cmd:02X}")
181
+
182
+
183
+ # =============================================================================
184
+ # Centronics Parallel Printer Protocol Decoder
185
+ # =============================================================================
186
+
187
+
188
+ @dataclass
189
+ class CentronicsFrame:
190
+ """A decoded Centronics printer frame.
191
+
192
+ Attributes:
193
+ timestamp: Frame timestamp in seconds.
194
+ data: Data byte value (0-255).
195
+ character: ASCII character (if printable).
196
+ control: Control signal states.
197
+ """
198
+
199
+ timestamp: float
200
+ data: int
201
+ character: str | None
202
+ control: dict[str, bool]
203
+
204
+
205
+ def decode_centronics(
206
+ data_lines: list[NDArray[np.bool_]], # D0-D7 (8 data lines)
207
+ strobe: NDArray[np.bool_], # Strobe signal (active low)
208
+ busy: NDArray[np.bool_], # Busy signal
209
+ ack: NDArray[np.bool_], # Acknowledge signal (active low)
210
+ sample_rate: float = 1.0,
211
+ *,
212
+ select: NDArray[np.bool_] | None = None, # Select signal
213
+ paper_out: NDArray[np.bool_] | None = None, # Paper Out signal
214
+ error: NDArray[np.bool_] | None = None, # Error signal
215
+ ) -> list[CentronicsFrame]:
216
+ """Decode Centronics parallel printer protocol.
217
+
218
+ Args:
219
+ data_lines: List of 8 digital traces for D0-D7.
220
+ strobe: Strobe signal (active low).
221
+ busy: Busy signal (high when printer busy).
222
+ ack: Acknowledge signal (active low pulse).
223
+ sample_rate: Sample rate in Hz.
224
+ select: Optional select signal.
225
+ paper_out: Optional paper out signal.
226
+ error: Optional error signal.
227
+
228
+ Returns:
229
+ List of CentronicsFrame objects.
230
+
231
+ Example:
232
+ >>> frames = decode_centronics(data, strobe, busy, ack, 1e6)
233
+ >>> for frame in frames:
234
+ ... if frame.character:
235
+ ... print(frame.character, end='')
236
+ """
237
+ if len(data_lines) != 8:
238
+ raise ValueError("Centronics requires exactly 8 data lines")
239
+
240
+ frames: list[CentronicsFrame] = []
241
+ time_base = 1.0 / sample_rate
242
+
243
+ # Combine data lines into bytes
244
+ data_bus = np.zeros(len(data_lines[0]), dtype=np.uint8)
245
+ for i, line in enumerate(data_lines):
246
+ data_bus |= (line.astype(np.uint8) << i).astype(np.uint8)
247
+
248
+ # Detect STROBE falling edges (data latch)
249
+ strobe_falling = np.where(np.diff(strobe.astype(np.int8)) == -1)[0]
250
+
251
+ for idx in strobe_falling:
252
+ # Sample data after falling edge
253
+ sample_idx = idx + 1
254
+ if sample_idx >= len(data_bus):
255
+ continue
256
+
257
+ timestamp = idx * time_base
258
+ data_byte = int(data_bus[sample_idx])
259
+
260
+ # Check if printable ASCII
261
+ char = chr(data_byte) if 32 <= data_byte <= 126 else None
262
+
263
+ # Capture control signals
264
+ control = {
265
+ "busy": bool(busy[sample_idx]),
266
+ "ack": not ack[sample_idx], # Active low
267
+ }
268
+
269
+ if select is not None:
270
+ control["select"] = bool(select[sample_idx])
271
+ if paper_out is not None:
272
+ control["paper_out"] = bool(paper_out[sample_idx])
273
+ if error is not None:
274
+ control["error"] = bool(error[sample_idx])
275
+
276
+ frames.append(
277
+ CentronicsFrame(
278
+ timestamp=timestamp,
279
+ data=data_byte,
280
+ character=char,
281
+ control=control,
282
+ )
283
+ )
284
+
285
+ return frames
286
+
287
+
288
+ # =============================================================================
289
+ # ISA Bus Protocol Analyzer
290
+ # =============================================================================
291
+
292
+
293
+ class ISACycleType(Enum):
294
+ """ISA bus cycle types."""
295
+
296
+ MEMORY_READ = "memory_read"
297
+ MEMORY_WRITE = "memory_write"
298
+ IO_READ = "io_read"
299
+ IO_WRITE = "io_write"
300
+ DMA = "dma"
301
+ INTERRUPT = "interrupt"
302
+
303
+
304
+ @dataclass
305
+ class ISATransaction:
306
+ """An ISA bus transaction.
307
+
308
+ Attributes:
309
+ timestamp: Transaction timestamp in seconds.
310
+ cycle_type: Type of bus cycle.
311
+ address: Address (20-bit for memory, 16-bit for I/O).
312
+ data: Data byte (if applicable).
313
+ description: Human-readable description.
314
+ """
315
+
316
+ timestamp: float
317
+ cycle_type: ISACycleType
318
+ address: int
319
+ data: int | None
320
+ description: str
321
+
322
+
323
+ def decode_isa_bus(
324
+ address_lines: list[NDArray[np.bool_]], # SA0-SA19 (20 address lines)
325
+ data_lines: list[NDArray[np.bool_]], # SD0-SD7 (8 data lines)
326
+ ior: NDArray[np.bool_], # I/O Read (active low)
327
+ iow: NDArray[np.bool_], # I/O Write (active low)
328
+ memr: NDArray[np.bool_], # Memory Read (active low)
329
+ memw: NDArray[np.bool_], # Memory Write (active low)
330
+ ale: NDArray[np.bool_], # Address Latch Enable
331
+ sample_rate: float = 1.0,
332
+ ) -> list[ISATransaction]:
333
+ """Decode ISA bus transactions.
334
+
335
+ Args:
336
+ address_lines: List of 20 digital traces for SA0-SA19.
337
+ data_lines: List of 8 digital traces for SD0-SD7.
338
+ ior: I/O Read signal (active low).
339
+ iow: I/O Write signal (active low).
340
+ memr: Memory Read signal (active low).
341
+ memw: Memory Write signal (active low).
342
+ ale: Address Latch Enable.
343
+ sample_rate: Sample rate in Hz.
344
+
345
+ Returns:
346
+ List of ISATransaction objects.
347
+
348
+ Example:
349
+ >>> trans = decode_isa_bus(addr, data, ior, iow, memr, memw, ale, 1e6)
350
+ >>> for t in trans:
351
+ ... print(f"{t.timestamp*1e6:.1f}us: {t.description}")
352
+ """
353
+ if len(address_lines) < 16:
354
+ raise ValueError("ISA bus requires at least 16 address lines")
355
+ if len(data_lines) != 8:
356
+ raise ValueError("ISA bus requires exactly 8 data lines")
357
+
358
+ transactions: list[ISATransaction] = []
359
+ time_base = 1.0 / sample_rate
360
+
361
+ # Combine address lines
362
+ address_bus = np.zeros(len(address_lines[0]), dtype=np.uint32)
363
+ for i, line in enumerate(address_lines):
364
+ address_bus |= (line.astype(np.uint32) << i).astype(np.uint32)
365
+
366
+ # Combine data lines
367
+ data_bus = np.zeros(len(data_lines[0]), dtype=np.uint8)
368
+ for i, line in enumerate(data_lines):
369
+ data_bus |= (line.astype(np.uint8) << i).astype(np.uint8)
370
+
371
+ # Detect ALE falling edges (address latch)
372
+ ale_falling = np.where(np.diff(ale.astype(np.int8)) == -1)[0]
373
+
374
+ for idx in ale_falling:
375
+ if idx >= len(address_bus):
376
+ continue
377
+
378
+ timestamp = idx * time_base
379
+ address = int(address_bus[idx])
380
+
381
+ # Look ahead for read/write strobes (larger window to catch delayed strobes)
382
+ search_window = min(idx + 200, len(ior))
383
+
384
+ ior_active = np.any(~ior[idx:search_window])
385
+ iow_active = np.any(~iow[idx:search_window])
386
+ memr_active = np.any(~memr[idx:search_window])
387
+ memw_active = np.any(~memw[idx:search_window])
388
+
389
+ # Determine cycle type
390
+ data_val = None
391
+
392
+ if ior_active:
393
+ cycle_type = ISACycleType.IO_READ
394
+ desc = f"I/O Read from 0x{address:04X}"
395
+ # Find data at IOR falling edge
396
+ ior_idx = np.where(~ior[idx:search_window])[0]
397
+ if len(ior_idx) > 0:
398
+ data_val = int(data_bus[idx + ior_idx[0]])
399
+ desc += f" = 0x{data_val:02X}"
400
+
401
+ elif iow_active:
402
+ cycle_type = ISACycleType.IO_WRITE
403
+ iow_idx = np.where(~iow[idx:search_window])[0]
404
+ if len(iow_idx) > 0:
405
+ data_val = int(data_bus[idx + iow_idx[0]])
406
+ desc = f"I/O Write 0x{data_val:02X} to 0x{address:04X}"
407
+
408
+ elif memr_active:
409
+ cycle_type = ISACycleType.MEMORY_READ
410
+ desc = f"Memory Read from 0x{address:05X}"
411
+ memr_idx = np.where(~memr[idx:search_window])[0]
412
+ if len(memr_idx) > 0:
413
+ data_val = int(data_bus[idx + memr_idx[0]])
414
+ desc += f" = 0x{data_val:02X}"
415
+
416
+ elif memw_active:
417
+ cycle_type = ISACycleType.MEMORY_WRITE
418
+ memw_idx = np.where(~memw[idx:search_window])[0]
419
+ if len(memw_idx) > 0:
420
+ data_val = int(data_bus[idx + memw_idx[0]])
421
+ desc = f"Memory Write 0x{data_val:02X} to 0x{address:05X}"
422
+
423
+ else:
424
+ # No control signals active
425
+ continue
426
+
427
+ transactions.append(
428
+ ISATransaction(
429
+ timestamp=timestamp,
430
+ cycle_type=cycle_type,
431
+ address=address,
432
+ data=data_val,
433
+ description=desc,
434
+ )
435
+ )
436
+
437
+ return transactions
438
+
439
+
440
+ __all__ = [
441
+ "CentronicsFrame",
442
+ "GPIBFrame",
443
+ "GPIBMessageType",
444
+ "ISACycleType",
445
+ "ISATransaction",
446
+ "decode_centronics",
447
+ "decode_gpib",
448
+ "decode_isa_bus",
449
+ ]
@@ -0,0 +1,52 @@
1
+ """Side-channel analysis module.
2
+
3
+ This module provides research-grade implementations of side-channel analysis
4
+ techniques including Differential Power Analysis (DPA), Correlation Power
5
+ Analysis (CPA), and timing analysis.
6
+
7
+ Example:
8
+ >>> from oscura.analyzers.side_channel import DPAAnalyzer, CPAAnalyzer
9
+ >>> # DPA attack
10
+ >>> dpa = DPAAnalyzer(target_bit=0)
11
+ >>> result = dpa.analyze(traces, plaintexts)
12
+ >>> print(f"Key byte guess: 0x{result.key_guess:02X}")
13
+ >>>
14
+ >>> # CPA attack
15
+ >>> cpa = CPAAnalyzer(leakage_model="hamming_weight")
16
+ >>> result = cpa.analyze(traces, plaintexts)
17
+ >>> print(f"Correlation: {result.max_correlation:.4f}")
18
+
19
+ References:
20
+ Kocher et al. "Differential Power Analysis" (CRYPTO 1999)
21
+ Brier et al. "Correlation Power Analysis" (CHES 2004)
22
+ """
23
+
24
+ from oscura.analyzers.side_channel.power import (
25
+ CPAAnalyzer,
26
+ CPAResult,
27
+ DPAAnalyzer,
28
+ DPAResult,
29
+ LeakageModel,
30
+ hamming_distance,
31
+ hamming_weight,
32
+ )
33
+ from oscura.analyzers.side_channel.timing import (
34
+ TimingAnalyzer,
35
+ TimingAttackResult,
36
+ TimingLeak,
37
+ )
38
+
39
+ __all__ = [
40
+ # Power analysis
41
+ "CPAAnalyzer",
42
+ "CPAResult",
43
+ "DPAAnalyzer",
44
+ "DPAResult",
45
+ "LeakageModel",
46
+ # Timing analysis
47
+ "TimingAnalyzer",
48
+ "TimingAttackResult",
49
+ "TimingLeak",
50
+ "hamming_distance",
51
+ "hamming_weight",
52
+ ]