aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJ08nY2021-01-20 20:24:11 +0100
committerJ08nY2021-01-20 20:24:11 +0100
commit9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3 (patch)
treebb695e485808e4d0517d84053019e2f7ddb03679
parentadc3cd52147f35e0a7cc9008ac96619dd89cda48 (diff)
downloadpyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.tar.gz
pyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.tar.zst
pyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.zip
-rw-r--r--Makefile2
-rw-r--r--docs/_static/custom.css4
-rw-r--r--docs/conf.py9
-rw-r--r--pyecsca/ec/configuration.py3
-rw-r--r--pyecsca/ec/context.py14
-rw-r--r--pyecsca/ec/coordinates.py3
-rw-r--r--pyecsca/ec/curve.py11
-rw-r--r--pyecsca/ec/error.py3
-rw-r--r--pyecsca/ec/formula.py19
-rw-r--r--pyecsca/ec/key_agreement.py7
-rw-r--r--pyecsca/ec/key_generation.py13
-rw-r--r--pyecsca/ec/mod.py71
-rw-r--r--pyecsca/ec/model.py30
-rw-r--r--pyecsca/ec/mult.py7
-rw-r--r--pyecsca/ec/naf.py3
-rw-r--r--pyecsca/ec/op.py3
-rw-r--r--pyecsca/ec/params.py4
-rw-r--r--pyecsca/ec/point.py7
-rw-r--r--pyecsca/ec/signature.py4
-rw-r--r--pyecsca/ec/transformations.py3
-rw-r--r--pyecsca/misc/__init__.py2
-rw-r--r--pyecsca/misc/cfg.py32
-rw-r--r--pyecsca/sca/re/rpa.py6
-rw-r--r--pyecsca/sca/scope/base.py4
-rw-r--r--pyecsca/sca/scope/chipwhisperer.py3
-rw-r--r--pyecsca/sca/scope/picoscope_alt.py11
-rw-r--r--pyecsca/sca/scope/picoscope_sdk.py32
-rw-r--r--pyecsca/sca/target/ISO7816.py4
-rw-r--r--pyecsca/sca/target/PCSC.py3
-rw-r--r--pyecsca/sca/target/base.py3
-rw-r--r--pyecsca/sca/target/binary.py6
-rw-r--r--pyecsca/sca/target/chipwhisperer.py13
-rw-r--r--pyecsca/sca/target/ectester.py19
-rw-r--r--pyecsca/sca/target/flash.py9
-rw-r--r--pyecsca/sca/target/serial.py19
-rw-r--r--pyecsca/sca/target/simpleserial.py14
-rw-r--r--pyecsca/sca/trace/align.py10
-rw-r--r--pyecsca/sca/trace/combine.py3
-rw-r--r--pyecsca/sca/trace/edit.py3
-rw-r--r--pyecsca/sca/trace/filter.py15
-rw-r--r--pyecsca/sca/trace/match.py24
-rw-r--r--pyecsca/sca/trace/process.py23
-rw-r--r--pyecsca/sca/trace/sampling.py3
-rw-r--r--pyecsca/sca/trace/test.py9
-rw-r--r--pyecsca/sca/trace/trace.py22
-rw-r--r--pyecsca/sca/trace_set/base.py5
-rw-r--r--pyecsca/sca/trace_set/chipwhisperer.py4
-rw-r--r--pyecsca/sca/trace_set/hdf5.py7
-rw-r--r--pyecsca/sca/trace_set/inspector.py3
-rw-r--r--pyecsca/sca/trace_set/pickle.py6
-rw-r--r--setup.py3
51 files changed, 462 insertions, 78 deletions
diff --git a/Makefile b/Makefile
index b5457ae..ff67a45 100644
--- a/Makefile
+++ b/Makefile
@@ -29,7 +29,7 @@ codestyle-all:
flake8 --ignore=E501,F405,F403,F401,E126 pyecsca test
doc-coverage:
- interrogate -vv -nmps pyecsca
+ interrogate -vv -nmps -e pyecsca/ec/std/.github/ pyecsca
docs:
$(MAKE) -C docs apidoc
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index 25e59f9..129bd44 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -4,4 +4,8 @@ div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 {
img.logo {
max-width: 40%;
+}
+
+dl.class, dl.function {
+ margin-bottom: 15px;
} \ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 5ceee32..438d83b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,7 +23,7 @@ project = 'pyecsca'
copyright = '2018-2020, Jan Jancar'
author = 'Jan Jancar'
-import sys; import os; sys.path.append(os.path.abspath('..'))
+sys.path.append(os.path.abspath('..'))
# The short X.Y version
version = '0.1.0'
@@ -175,7 +175,7 @@ man_pages = [
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pyecsca', 'pyecsca Documentation',
- author, 'pyecsca', 'One line description of project.',
+ author, 'pyecsca', 'Python Elliptic Curve Side-Channel Analysis toolkit',
'Miscellaneous'),
]
@@ -207,7 +207,10 @@ autodoc_default_options = {
"undoc-members": True,
"inherited-members": True,
"show-inheritance": True,
- "member-order": "bysource"
+ "member-order": "bysource",
+ # "special-members": "__init__"
}
+autoclass_content = "both"
+
nbsphinx_allow_errors = True
diff --git a/pyecsca/ec/configuration.py b/pyecsca/ec/configuration.py
index 190ed61..04f8fc7 100644
--- a/pyecsca/ec/configuration.py
+++ b/pyecsca/ec/configuration.py
@@ -1,3 +1,6 @@
+"""
+This module provides a way to work with and enumerate implementation configurations.
+"""
from dataclasses import dataclass
from enum import Enum
from itertools import product
diff --git a/pyecsca/ec/context.py b/pyecsca/ec/context.py
index 39bf6f0..3b38584 100644
--- a/pyecsca/ec/context.py
+++ b/pyecsca/ec/context.py
@@ -1,3 +1,16 @@
+"""
+This module provides classes for tracing the execution of operations (key generation, scalar multiplication, formula
+execution, operation evaluation). These operations are traced in `Context` classes using `Actions`. Different contexts
+trace actions differently.
+
+A :py:class:`DefaultContext` traces actions into a tree as they are executed (a scalar
+multiplication actions has as its children an ordered list of the individual formula executions it has done).
+
+A :py:class:`PathContext` works like a :py:class:`DefaultContext` that only traces an action on a particular path
+in the tree.
+
+A :py:class:`NullContext` does not trace any actions and is the default context.
+"""
from abc import abstractmethod, ABC
from collections import OrderedDict
from contextvars import ContextVar, Token
@@ -54,6 +67,7 @@ class ResultAction(Action):
@public
class Tree(OrderedDict):
+ """A recursively-implemented tree."""
def get_by_key(self, path: List) -> Any:
"""
diff --git a/pyecsca/ec/coordinates.py b/pyecsca/ec/coordinates.py
index 6e001a9..299cdbf 100644
--- a/pyecsca/ec/coordinates.py
+++ b/pyecsca/ec/coordinates.py
@@ -1,3 +1,6 @@
+"""
+This module provides a coordinate model class.
+"""
from ast import parse, Module
from os.path import join
from typing import List, Any, MutableMapping
diff --git a/pyecsca/ec/curve.py b/pyecsca/ec/curve.py
index e976289..4cfb978 100644
--- a/pyecsca/ec/curve.py
+++ b/pyecsca/ec/curve.py
@@ -1,3 +1,6 @@
+"""
+This module provides an elliptic curve class.
+"""
from ast import Module
from copy import copy
from typing import MutableMapping, Union, List, Optional
@@ -63,7 +66,7 @@ class EllipticCurve(object):
def affine_add(self, one: Point, other: Point) -> Point:
"""
Add two affine points using the affine addition formula.
- Handles the case of point at infinity gracefully.
+ Handles the case of point at infinity gracefully (short-circuits).
:param one: One point.
:param other: Another point.
@@ -80,7 +83,7 @@ class EllipticCurve(object):
def affine_double(self, one: Point) -> Point:
"""
Double an affine point using the affine doubling formula.
- Handles the case of point at infinity gracefully.
+ Handles the case of point at infinity gracefully (short-circuits).
:param one: A point.
:return: The doubling of the point.
@@ -92,7 +95,7 @@ class EllipticCurve(object):
def affine_negate(self, one: Point) -> Point:
"""
Negate an affine point using the affine negation formula.
- Handles the case of point at infinity gracefully.
+ Handles the case of point at infinity gracefully (short-circuits).
:param one: A point.
:return: The negation of the point.
@@ -104,7 +107,7 @@ class EllipticCurve(object):
def affine_multiply(self, point: Point, scalar: int) -> Point:
"""
Multiply an affine point by a scalar using the affine doubling and addition formulas.
- Handles the case of point at infinity gracefully.
+ Handles the case of point at infinity gracefully (short-circuits).
:param point: The point to multiply.
:param scalar: The scalar to use.
diff --git a/pyecsca/ec/error.py b/pyecsca/ec/error.py
index ecbe4d2..26e7945 100644
--- a/pyecsca/ec/error.py
+++ b/pyecsca/ec/error.py
@@ -1,3 +1,6 @@
+"""
+This module contains exceptions and warnings used in the library.
+"""
from public import public
from ..misc.cfg import getconfig
diff --git a/pyecsca/ec/formula.py b/pyecsca/ec/formula.py
index 7a2736a..623a52a 100644
--- a/pyecsca/ec/formula.py
+++ b/pyecsca/ec/formula.py
@@ -1,3 +1,6 @@
+"""
+This module provides an abstract base class of a formula along with concrete instantiations.
+"""
from abc import ABC, abstractmethod
from ast import parse, Expression
from astunparse import unparse
@@ -287,6 +290,7 @@ class Formula(ABC):
class EFDFormula(Formula):
+ """A formula from the `Explicit-Formulas Database <https://www.hyperelliptic.org/EFD/>`_."""
def __init__(self, path: str, name: str, coordinate_model: Any):
self.name = name
@@ -351,6 +355,7 @@ class EFDFormula(Formula):
@public
class AdditionFormula(Formula, ABC):
+ """A formula that adds two points."""
shortname = "add"
num_inputs = 2
num_outputs = 1
@@ -363,6 +368,7 @@ class AdditionEFDFormula(AdditionFormula, EFDFormula):
@public
class DoublingFormula(Formula, ABC):
+ """A formula that doubles a point."""
shortname = "dbl"
num_inputs = 1
num_outputs = 1
@@ -375,6 +381,7 @@ class DoublingEFDFormula(DoublingFormula, EFDFormula):
@public
class TriplingFormula(Formula, ABC):
+ """A formula that triples a point."""
shortname = "tpl"
num_inputs = 1
num_outputs = 1
@@ -387,6 +394,7 @@ class TriplingEFDFormula(TriplingFormula, EFDFormula):
@public
class NegationFormula(Formula, ABC):
+ """A formula that negates a point."""
shortname = "neg"
num_inputs = 1
num_outputs = 1
@@ -399,6 +407,7 @@ class NegationEFDFormula(NegationFormula, EFDFormula):
@public
class ScalingFormula(Formula, ABC):
+ """A formula that somehow scales the point (to a given representative of a projective class)."""
shortname = "scl"
num_inputs = 1
num_outputs = 1
@@ -411,6 +420,10 @@ class ScalingEFDFormula(ScalingFormula, EFDFormula):
@public
class DifferentialAdditionFormula(Formula, ABC):
+ """
+ A differential addition formula that adds two points with a known difference.
+ The first input point is the difference of the third input and the second input (`P[0] = P[2] - P[1]`).
+ """
shortname = "dadd"
num_inputs = 3
num_outputs = 1
@@ -423,6 +436,12 @@ class DifferentialAdditionEFDFormula(DifferentialAdditionFormula, EFDFormula):
@public
class LadderFormula(Formula, ABC):
+ """
+ A ladder formula for simultaneous addition of two points and doubling of the one of them, with a known difference.
+ The first input point is the difference of the third input and the second input (`P[0] = P[2] - P[1]`).
+ The first output point is the doubling of the second input point (`O[0] = 2 * P[1]`).
+ The second output point is the addition of the second and third input points (`O[1] = P[1] + P[2]`).
+ """
shortname = "ladd"
num_inputs = 3
num_outputs = 2
diff --git a/pyecsca/ec/key_agreement.py b/pyecsca/ec/key_agreement.py
index 7d139fe..c9e4a01 100644
--- a/pyecsca/ec/key_agreement.py
+++ b/pyecsca/ec/key_agreement.py
@@ -1,3 +1,6 @@
+"""
+This module provides an implementation of ECDH (Elliptic Curve Diffie-Hellman).
+"""
import hashlib
from typing import Optional, Any
@@ -18,9 +21,7 @@ class ECDHAction(ResultAction):
privkey: Mod
pubkey: Point
- def __init__(self, params: DomainParameters, hash_algo: Optional[Any],
- privkey: Mod,
- pubkey: Point):
+ def __init__(self, params: DomainParameters, hash_algo: Optional[Any], privkey: Mod, pubkey: Point):
super().__init__()
self.params = params
self.hash_algo = hash_algo
diff --git a/pyecsca/ec/key_generation.py b/pyecsca/ec/key_generation.py
index 6476cac..d506585 100644
--- a/pyecsca/ec/key_generation.py
+++ b/pyecsca/ec/key_generation.py
@@ -1,3 +1,6 @@
+"""
+This module provides a key generator for elliptic curve keypairs.
+"""
from typing import Tuple
from public import public
@@ -30,12 +33,22 @@ class KeyGeneration(object):
affine: bool
def __init__(self, mult: ScalarMultiplier, params: DomainParameters, affine: bool = False):
+ """
+ :param mult: The scalar multiplier to use during key generation.
+ :param params: The domain parameters over which to generate the keypair.
+ :param affine: Whether to transform the public key point to the affine form during key generation.
+ """
self.mult = mult
self.params = params
self.mult.init(self.params, self.params.generator)
self.affine = affine
def generate(self) -> Tuple[Mod, Point]:
+ """
+ Generate a keypair.
+
+ :return: The generated keypair, a `tuple` of the private key (scalar) and the public key (point).
+ """
with KeygenAction(self.params) as action:
privkey = Mod.random(self.params.order)
pubkey = self.mult.multiply(privkey.x)
diff --git a/pyecsca/ec/mod.py b/pyecsca/ec/mod.py
index c051e20..5d3ffc0 100644
--- a/pyecsca/ec/mod.py
+++ b/pyecsca/ec/mod.py
@@ -1,10 +1,17 @@
+"""
+This module provides several implementations of an element of ℤₙ. The base class :py:class:`Mod` dynamically
+dispatches to the implementation chosen by the runtime configuration of the library
+(see :py:class:`pyecsca.misc.cfg.Config`). A Python integer based implementation is available under
+:py:class:`RawMod`. A symbolic implementation based on sympy is available under :py:class:`SymbolicMod`. If
+`gmpy2` is installed, a GMP based implementation is available under :py:class:`GMPMod`.
+"""
import random
import secrets
from functools import wraps, lru_cache
from typing import Type, Dict
from public import public
-from sympy import Expr, Mod as SympyMod, FF
+from sympy import Expr, FF
from .error import raise_non_invertible, raise_non_residue
from .context import ResultAction
@@ -80,7 +87,7 @@ def miller_rabin(n: int, rounds: int = 50) -> bool:
return True
-def check(func):
+def _check(func):
@wraps(func)
def method(self, other):
if type(self) is not type(other):
@@ -111,6 +118,7 @@ _mod_classes: Dict[str, Type] = {}
@public
class Mod(object):
+ """An element x of ℤₙ."""
def __new__(cls, *args, **kwargs):
if cls != Mod:
@@ -127,19 +135,19 @@ class Mod(object):
self.x = x
self.n = n
- @check
+ @_check
def __add__(self, other):
return self.__class__((self.x + other.x) % self.n, self.n)
- @check
+ @_check
def __radd__(self, other):
return self + other
- @check
+ @_check
def __sub__(self, other):
return self.__class__((self.x - other.x) % self.n, self.n)
- @check
+ @_check
def __rsub__(self, other):
return -self + other
@@ -170,31 +178,31 @@ class Mod(object):
"""
...
- @check
+ @_check
def __mul__(self, other):
return self.__class__((self.x * other.x) % self.n, self.n)
- @check
+ @_check
def __rmul__(self, other):
return self * other
- @check
+ @_check
def __truediv__(self, other):
return self * ~other
- @check
+ @_check
def __rtruediv__(self, other):
return ~self * other
- @check
+ @_check
def __floordiv__(self, other):
return self * ~other
- @check
+ @_check
def __rfloordiv__(self, other):
return ~self * other
- @check
+ @_check
def __divmod__(self, divisor):
q, r = divmod(self.x, divisor.x)
return self.__class__(q, self.n), self.__class__(r, self.n)
@@ -225,7 +233,7 @@ class Mod(object):
@public
class RawMod(Mod):
- """An element x of ℤₙ."""
+ """An element x of ℤₙ (implemented using Python integers)."""
x: int
n: int
@@ -330,6 +338,8 @@ _mod_classes["python"] = RawMod
@public
class Undefined(Mod):
+ """A special undefined element."""
+
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
@@ -406,12 +416,17 @@ class Undefined(Mod):
raise NotImplementedError
-def symbolic_check(func):
+@lru_cache
+def __ff_cache(n):
+ return FF(n)
+
+
+def _symbolic_check(func):
@wraps(func)
def method(self, other):
if type(self) is not type(other):
if type(other) is int:
- other = self.__class__(FF(self.n)(other), self.n)
+ other = self.__class__(__ff_cache(self.n)(other), self.n)
else:
other = self.__class__(other, self.n)
else:
@@ -424,7 +439,7 @@ def symbolic_check(func):
@public
class SymbolicMod(Mod):
- """A symbolic element x of ℤₙ."""
+ """A symbolic element x of ℤₙ (implemented using sympy)."""
x: Expr
n: int
@@ -434,19 +449,19 @@ class SymbolicMod(Mod):
def __init__(self, x: Expr, n: int):
super().__init__(x, n)
- @symbolic_check
+ @_symbolic_check
def __add__(self, other):
return self.__class__((self.x + other.x), self.n)
- @symbolic_check
+ @_symbolic_check
def __radd__(self, other):
return self + other
- @symbolic_check
+ @_symbolic_check
def __sub__(self, other):
return self.__class__((self.x - other.x), self.n)
- @symbolic_check
+ @_symbolic_check
def __rsub__(self, other):
return -self + other
@@ -465,27 +480,27 @@ class SymbolicMod(Mod):
def __invert__(self):
return self.inverse()
- @symbolic_check
+ @_symbolic_check
def __mul__(self, other):
return self.__class__(self.x * other.x, self.n)
- @symbolic_check
+ @_symbolic_check
def __rmul__(self, other):
return self * other
- @symbolic_check
+ @_symbolic_check
def __truediv__(self, other):
return self * ~other
- @symbolic_check
+ @_symbolic_check
def __rtruediv__(self, other):
return ~self * other
- @symbolic_check
+ @_symbolic_check
def __floordiv__(self, other):
return self * ~other
- @symbolic_check
+ @_symbolic_check
def __rfloordiv__(self, other):
return ~self * other
@@ -596,7 +611,7 @@ if has_gmp:
r *= b
return r
- @check
+ @_check
def __divmod__(self, divisor):
q, r = gmpy2.f_divmod(self.x, divisor.x)
return GMPMod(q, self.n), GMPMod(r, self.n)
diff --git a/pyecsca/ec/model.py b/pyecsca/ec/model.py
index bfb9184..4ddbafc 100644
--- a/pyecsca/ec/model.py
+++ b/pyecsca/ec/model.py
@@ -1,3 +1,6 @@
+"""
+This module provides curve model classes for the supported curve models.
+"""
from ast import parse, Expression, Module
from os.path import join
from typing import List, MutableMapping
@@ -110,6 +113,13 @@ class EFDCurveModel(CurveModel):
@public
class ShortWeierstrassModel(EFDCurveModel):
+ """
+ A short-Weierstrass curve model, with the equation:
+
+ .. math::
+
+ y^2 = x^3 + a x + b
+ """
def __init__(self):
super().__init__("shortw")
@@ -117,13 +127,26 @@ class ShortWeierstrassModel(EFDCurveModel):
@public
class MontgomeryModel(EFDCurveModel):
+ """
+ A Montgomery curve model, with the equation:
+ .. math::
+
+ B y^2 = x^3 + A x^2 + x
+ """
def __init__(self):
super().__init__("montgom")
@public
class EdwardsModel(EFDCurveModel):
+ """
+ An Edwards curve model, with the equation:
+
+ .. math::
+
+ x^2 + y^2 = c^2 (1 + d x^2 y^2)
+ """
def __init__(self):
super().__init__("edwards")
@@ -131,6 +154,13 @@ class EdwardsModel(EFDCurveModel):
@public
class TwistedEdwardsModel(EFDCurveModel):
+ """
+ A twisted-Edwards curve model, with the equation:
+
+ .. math::
+
+ a x^2 + y^2 = 1 + d x^2 y^2
+ """
def __init__(self):
super().__init__("twisted")
diff --git a/pyecsca/ec/mult.py b/pyecsca/ec/mult.py
index 1650736..6fd6feb 100644
--- a/pyecsca/ec/mult.py
+++ b/pyecsca/ec/mult.py
@@ -1,3 +1,6 @@
+"""
+This module provides several classes implementing different scalar multiplication algorithms.
+"""
from abc import ABC, abstractmethod
from copy import copy
from typing import Mapping, Tuple, Optional, MutableMapping, ClassVar, Set, Type
@@ -29,7 +32,7 @@ class ScalarMultiplicationAction(ResultAction):
@public
class PrecomputationAction(Action):
- """"""
+ """A precomputation of a point in scalar multiplication."""
params: DomainParameters
point: Point
@@ -53,7 +56,9 @@ class ScalarMultiplier(ABC):
optionals: ClassVar[Set[Type]] # Type[Formula] but mypy has a false positive
"""The optional set of formulas that the multiplier can use."""
short_circuit: bool
+ """Whether the formulas will short-circuit upon input of the point at infinity."""
formulas: Mapping[str, Formula]
+ """All formulas the multiplier was initialized with."""
_params: DomainParameters
_point: Point
_initialized: bool = False
diff --git a/pyecsca/ec/naf.py b/pyecsca/ec/naf.py
index fe4f24c..9d01555 100644
--- a/pyecsca/ec/naf.py
+++ b/pyecsca/ec/naf.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for computing the Non-Adjacent Form (NAF) of integers.
+"""
from public import public
from typing import List
diff --git a/pyecsca/ec/op.py b/pyecsca/ec/op.py
index ee23e51..2401624 100644
--- a/pyecsca/ec/op.py
+++ b/pyecsca/ec/op.py
@@ -1,3 +1,6 @@
+"""
+This module provides a class for a code operation.
+"""
from ast import (Module, walk, Name, BinOp, UnaryOp, Constant, Mult, Div, Add, Sub, Pow, Assign,
operator as ast_operator, unaryop as ast_unaryop, USub)
from enum import Enum
diff --git a/pyecsca/ec/params.py b/pyecsca/ec/params.py
index a48b70d..6583bae 100644
--- a/pyecsca/ec/params.py
+++ b/pyecsca/ec/params.py
@@ -1,3 +1,7 @@
+"""
+This module provides functions for obtaining domain parameters from the `std-curves <https://github.com/J08nY/std-curves>`_
+repository. It also provides a domain parameter class and a class for a whole category of domain parameters.
+"""
import json
from sympy import Poly, FF, symbols, sympify
from astunparse import unparse
diff --git a/pyecsca/ec/point.py b/pyecsca/ec/point.py
index ae6ded9..6ba6469 100644
--- a/pyecsca/ec/point.py
+++ b/pyecsca/ec/point.py
@@ -1,5 +1,8 @@
+"""
+This module provides a `Point` class and a special `InfinityPoint` class for the point at infinity.
+"""
from copy import copy
-from typing import Mapping, TYPE_CHECKING, Optional
+from typing import Mapping, TYPE_CHECKING
from public import public
@@ -7,6 +10,8 @@ from .context import ResultAction
from .coordinates import AffineCoordinateModel, CoordinateModel, EFDCoordinateModel
from .mod import Mod, Undefined
from .op import CodeOp
+
+
if TYPE_CHECKING:
from .curve import EllipticCurve
diff --git a/pyecsca/ec/signature.py b/pyecsca/ec/signature.py
index a97594b..9df6599 100644
--- a/pyecsca/ec/signature.py
+++ b/pyecsca/ec/signature.py
@@ -1,5 +1,7 @@
+"""
+This module provides an implementation of ECDSA (Elliptic Curve Digital Signature Algorithm).
+"""
import hashlib
-import secrets
from typing import Optional, Any
from asn1crypto.core import Sequence, SequenceOf, Integer
diff --git a/pyecsca/ec/transformations.py b/pyecsca/ec/transformations.py
index 1363483..6e3b24c 100644
--- a/pyecsca/ec/transformations.py
+++ b/pyecsca/ec/transformations.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for transforming curves to different models.
+"""
from public import public
from .coordinates import AffineCoordinateModel
diff --git a/pyecsca/misc/__init__.py b/pyecsca/misc/__init__.py
index 46334eb..f093677 100644
--- a/pyecsca/misc/__init__.py
+++ b/pyecsca/misc/__init__.py
@@ -1 +1 @@
-"""Miscellaneous things."""
+"""package for miscellaneous things."""
diff --git a/pyecsca/misc/cfg.py b/pyecsca/misc/cfg.py
index 1c1d214..df3cce1 100644
--- a/pyecsca/misc/cfg.py
+++ b/pyecsca/misc/cfg.py
@@ -1,3 +1,7 @@
+"""
+This module provides functions for runtime configuration of the toolkit, such as how errors are handled, or which
+:py:class:`Mod` implementation is used.
+"""
from copy import deepcopy
from contextvars import ContextVar, Token
@@ -119,21 +123,47 @@ _config: ContextVar[Config] = ContextVar("config", default=Config())
@public
def getconfig() -> Config:
+ """
+ Get the current config.
+
+ :return: The current config.
+ """
return _config.get()
@public
def setconfig(cfg: Config) -> Token:
+ """
+ Set the current config.
+
+ :param cfg: The config to set.
+ :return: A token that can be used to reset the config to the previous one.
+ """
return _config.set(cfg)
@public
-def resetconfig(token: Token):
+def resetconfig(token: Token) -> None:
+ """
+ Reset the config to the previous one.
+
+ :param token: A token from :py:func:`setconfig()`.
+ """
_config.reset(token)
@public
class TemporaryConfig(object):
+ """
+ A temporary config context manager, can be entered as follows:
+
+ .. code-block:: python
+
+ with TemporaryConfig() as cfg:
+ cfg.some_property = some_value
+ ...
+ """
+
def __init__(self):
self.new_config = deepcopy(getconfig())
diff --git a/pyecsca/sca/re/rpa.py b/pyecsca/sca/re/rpa.py
index 73ae569..3dc00e2 100644
--- a/pyecsca/sca/re/rpa.py
+++ b/pyecsca/sca/re/rpa.py
@@ -1,3 +1,9 @@
+"""
+This module provides functionality inspired by the Refined-Power Analysis attack by Goubin:
+
+ A Refined Power-Analysis Attack on Elliptic Curve Cryptosystems, Louis Goubin, PKC '03
+ `<https://dl.acm.org/doi/10.5555/648120.747060>`_
+"""
from public import public
from typing import MutableMapping, Optional
diff --git a/pyecsca/sca/scope/base.py b/pyecsca/sca/scope/base.py
index 35c624f..68d60dd 100644
--- a/pyecsca/sca/scope/base.py
+++ b/pyecsca/sca/scope/base.py
@@ -1,3 +1,6 @@
+"""
+This module provides an abstract base class for oscilloscopes.
+"""
from enum import Enum, auto
from typing import Tuple, Sequence, Optional
@@ -8,6 +11,7 @@ from ..trace import Trace
@public
class SampleType(Enum):
+ """The sample unit."""
Raw = auto()
Volt = auto()
diff --git a/pyecsca/sca/scope/chipwhisperer.py b/pyecsca/sca/scope/chipwhisperer.py
index b516ecd..c5c21de 100644
--- a/pyecsca/sca/scope/chipwhisperer.py
+++ b/pyecsca/sca/scope/chipwhisperer.py
@@ -1,3 +1,6 @@
+"""
+This module provides an oscilloscope class using the ChipWhisperer-Lite scope.
+"""
from typing import Optional, Tuple, Sequence, Set
import numpy as np
diff --git a/pyecsca/sca/scope/picoscope_alt.py b/pyecsca/sca/scope/picoscope_alt.py
index 2c05c5e..544ef35 100644
--- a/pyecsca/sca/scope/picoscope_alt.py
+++ b/pyecsca/sca/scope/picoscope_alt.py
@@ -1,3 +1,7 @@
+"""
+This module provides an oscilloscope class for the PicoScope branded oscilloscopes using
+the alternative `pico-python <https://github.com/colinoflynn/pico-python>`_ bindings.
+"""
from time import time_ns, sleep
import numpy as np
from typing import Optional, Tuple, Sequence, Union
@@ -14,8 +18,15 @@ from ..trace import Trace
@public
class PicoScopeAlt(Scope): # pragma: no cover
+ """A PicoScope based scope. Supports series 3000,4000,5000 and 6000."""
def __init__(self, ps: Union[PS3000, PS4000, PS5000, PS6000]):
+ """
+ Create a new scope.
+
+ :param ps: An instance of one of the supported PicoScope classes (:py:class:`PS3000`, :py:class:`PS4000`,
+ :py:class:`PS5000`, :py:class:`PS6000`).
+ """
super().__init__()
self.ps = ps
self.trig_ratio: float = 0.0
diff --git a/pyecsca/sca/scope/picoscope_sdk.py b/pyecsca/sca/scope/picoscope_sdk.py
index 328a3ff..1a7979c 100644
--- a/pyecsca/sca/scope/picoscope_sdk.py
+++ b/pyecsca/sca/scope/picoscope_sdk.py
@@ -1,3 +1,7 @@
+"""
+This module provides an oscilloscope class for PicoScope branded oscilloscopes using
+the official `picosdk-python-wrappers <https://github.com/picotech/picosdk-python-wrappers>`_.
+"""
import ctypes
from math import log2, floor
from time import time_ns, sleep
@@ -31,6 +35,15 @@ from ..trace import Trace
def adc2volt(adc: Union[np.ndarray, ctypes.c_int16],
volt_range: float, adc_minmax: int, dtype=np.float32) -> Union[np.ndarray, float]: # pragma: no cover
+ """
+ Convert raw adc values to volts.
+
+ :param adc: Either a single value (:py:class:`ctypes.c_int16`) or an array (:py:class:`np.ndarray`) of those to convert.
+ :param volt_range: The voltage range used for collecting the samples.
+ :param adc_minmax:
+ :param dtype: The numpy `dtype` of the output.
+ :return: The converted values.
+ """
if isinstance(adc, ctypes.c_int16):
return (adc.value / adc_minmax) * volt_range
if isinstance(adc, np.ndarray):
@@ -40,6 +53,15 @@ def adc2volt(adc: Union[np.ndarray, ctypes.c_int16],
def volt2adc(volt: Union[np.ndarray, float],
volt_range: float, adc_minmax: int, dtype=np.float32) -> Union[np.ndarray, ctypes.c_int16]: # pragma: no cover
+ """
+ Convert volt values to raw adc values.
+
+ :param volt: Either a single value (:py:class:`float`) or an array (:py:class:`np.ndarray`) of those to convert.
+ :param volt_range: The voltage range used for collecting the samples.
+ :param adc_minmax:
+ :param dtype: The numpy `dtype` of the output.
+ :return: The converted values.
+ """
if isinstance(volt, float):
return ctypes.c_int16(int((volt / volt_range) * adc_minmax))
if isinstance(volt, np.ndarray):
@@ -100,7 +122,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
def set_channel(self, channel: str, enabled: bool, coupling: str, range: float, offset: float):
if offset != 0.0:
- raise ValueError("Offset not supported.")
+ raise ValueError("Nonzero offset not supported.")
assert_pico_ok(
self.__dispatch_call("SetChannel", self.handle, self.CHANNELS[channel], enabled,
self.COUPLING[coupling], self.RANGES[range]))
@@ -223,12 +245,14 @@ class PicoScopeSdk(Scope): # pragma: no cover
if isinstance(ps3000, CannotFindPicoSDKError):
@public
class PS3000Scope(PicoScopeSdk): # pragma: no cover
+ """A PicoScope 3000 series oscilloscope is not available. (Install `libps3000`)."""
def __init__(self, variant: Optional[str] = None):
super().__init__(variant)
raise ps3000
else: # pragma: no cover
@public
class PS3000Scope(PicoScopeSdk): # type: ignore
+ """A PicoScope 3000 series oscilloscope."""
MODULE = ps3000
PREFIX = "ps3000"
CHANNELS = {
@@ -279,12 +303,14 @@ else: # pragma: no cover
if isinstance(ps4000, CannotFindPicoSDKError):
@public
class PS4000Scope(PicoScopeSdk): # pragma: no cover
+ """A PicoScope 4000 series oscilloscope is not available. (Install `libps4000`)."""
def __init__(self, variant: Optional[str] = None):
super().__init__(variant)
raise ps4000
else: # pragma: no cover
@public
class PS4000Scope(PicoScopeSdk): # type: ignore
+ """A PicoScope 4000 series oscilloscope."""
MODULE = ps4000
PREFIX = "ps4000"
CHANNELS = {
@@ -332,12 +358,14 @@ else: # pragma: no cover
if isinstance(ps5000, CannotFindPicoSDKError):
@public
class PS5000Scope(PicoScopeSdk): # pragma: no cover
+ """A PicoScope 5000 series oscilloscope is not available. (Install `libps5000`)."""
def __init__(self, variant: Optional[str] = None):
super().__init__(variant)
raise ps5000
else: # pragma: no cover
@public
class PS5000Scope(PicoScopeSdk): # type: ignore
+ """A PicoScope 5000 series oscilloscope."""
MODULE = ps5000
PREFIX = "ps5000"
CHANNELS = {
@@ -377,12 +405,14 @@ else: # pragma: no cover
if isinstance(ps6000, CannotFindPicoSDKError):
@public
class PS6000Scope(PicoScopeSdk): # pragma: no cover
+ """A PicoScope 6000 series oscilloscope is not available. (Install `libps6000`)."""
def __init__(self, variant: Optional[str] = None):
super().__init__(variant)
raise ps6000
else: # pragma: no cover
@public
class PS6000Scope(PicoScopeSdk): # type: ignore
+ """A PicoScope 6000 series oscilloscope."""
MODULE = ps6000
PREFIX = "ps6000"
CHANNELS = {
diff --git a/pyecsca/sca/target/ISO7816.py b/pyecsca/sca/target/ISO7816.py
index 2bf37f9..233685e 100644
--- a/pyecsca/sca/target/ISO7816.py
+++ b/pyecsca/sca/target/ISO7816.py
@@ -1,3 +1,6 @@
+"""
+This module provides classes for working with ISO7816-4 APDUs and an abstract base class for an ISO7816-4 based target.
+"""
from abc import abstractmethod, ABC
from dataclasses import dataclass
from typing import Optional
@@ -86,6 +89,7 @@ class ISO7816Target(Target, ABC):
@public
class ISO7816:
+ """A bunch of ISO7816-4 constants (status words)."""
SW_FILE_FULL = 0x6A84
SW_UNKNOWN = 0x6F00
SW_CLA_NOT_SUPPORTED = 0x6E00
diff --git a/pyecsca/sca/target/PCSC.py b/pyecsca/sca/target/PCSC.py
index 6ffc8d4..a377751 100644
--- a/pyecsca/sca/target/PCSC.py
+++ b/pyecsca/sca/target/PCSC.py
@@ -1,3 +1,6 @@
+"""
+This module provides a smartcard target communicating via PC/SC (Personal Computer/Smart Card).
+"""
from typing import Union
from public import public
diff --git a/pyecsca/sca/target/base.py b/pyecsca/sca/target/base.py
index cc0436c..6747c66 100644
--- a/pyecsca/sca/target/base.py
+++ b/pyecsca/sca/target/base.py
@@ -1,3 +1,6 @@
+"""
+This module provides an abstract base class for targets.
+"""
from abc import ABC, abstractmethod
from public import public
diff --git a/pyecsca/sca/target/binary.py b/pyecsca/sca/target/binary.py
index 0455c75..3e2877b 100644
--- a/pyecsca/sca/target/binary.py
+++ b/pyecsca/sca/target/binary.py
@@ -1,3 +1,6 @@
+"""
+This module provides a binary target class which represents a target that is a runnable binary on the host.
+"""
import subprocess
from subprocess import Popen
from typing import Optional, Union, List
@@ -9,6 +12,7 @@ from .serial import SerialTarget
@public
class BinaryTarget(SerialTarget):
+ """A binary target that is runnable on the host and communicates using the stdin/stdout streams."""
binary: List[str]
process: Optional[Popen] = None
debug_output: bool
@@ -26,7 +30,7 @@ class BinaryTarget(SerialTarget):
self.process = Popen(self.binary, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, bufsize=1)
- def write(self, data: bytes):
+ def write(self, data: bytes) -> None:
if self.process is None:
raise ValueError
if self.debug_output:
diff --git a/pyecsca/sca/target/chipwhisperer.py b/pyecsca/sca/target/chipwhisperer.py
index 3dd5686..ac3fa42 100644
--- a/pyecsca/sca/target/chipwhisperer.py
+++ b/pyecsca/sca/target/chipwhisperer.py
@@ -1,4 +1,9 @@
-from typing import Optional
+"""
+This module provides a `ChipWhisperer <https://github.com/newaetech/chipwhisperer/>`_ target class.
+ChipWhisperer is a side-channel analysis tool and framework. A ChipWhisperer target is one
+that uses the ChipWhisperer's SimpleSerial communication protocol and is communicated with
+using ChipWhisperer-Lite or Pro.
+"""
from time import sleep
import chipwhisperer as cw
@@ -12,6 +17,10 @@ from .simpleserial import SimpleSerialTarget
@public
class ChipWhispererTarget(Flashable, SimpleSerialTarget): # pragma: no cover
+ """
+ A ChipWhisperer-based target, using the SimpleSerial protocol and communicating
+ using ChipWhisperer-Lite/Pro.
+ """
def __init__(self, target: SimpleSerial, scope: ScopeTemplate, programmer, **kwargs):
super().__init__()
@@ -32,7 +41,7 @@ class ChipWhispererTarget(Flashable, SimpleSerialTarget): # pragma: no cover
return False
return True
- def write(self, data: bytes):
+ def write(self, data: bytes) -> None:
self.target.flush()
self.target.write(data.decode())
diff --git a/pyecsca/sca/target/ectester.py b/pyecsca/sca/target/ectester.py
index 8391e32..6ab10be 100644
--- a/pyecsca/sca/target/ectester.py
+++ b/pyecsca/sca/target/ectester.py
@@ -1,3 +1,6 @@
+"""
+This module provides an `ECTester <https://github.com/crocs-muni/ECTester/>`_ target class.
+"""
from abc import ABC
from binascii import hexlify
from enum import IntEnum, IntFlag
@@ -45,6 +48,7 @@ class ShiftableFlag(IntFlag): # pragma: no cover
@public
class KeypairEnum(ShiftableFlag): # pragma: no cover
+ """ECTester's KeyPair type."""
KEYPAIR_LOCAL = 0x01
KEYPAIR_REMOTE = 0x02
KEYPAIR_BOTH = KEYPAIR_LOCAL | KEYPAIR_REMOTE
@@ -52,6 +56,7 @@ class KeypairEnum(ShiftableFlag): # pragma: no cover
@public
class InstructionEnum(IntEnum): # pragma: no cover
+ """ECTester's instruction (INS)."""
INS_ALLOCATE = 0x5a
INS_CLEAR = 0x5b
INS_SET = 0x5c
@@ -74,12 +79,14 @@ class InstructionEnum(IntEnum): # pragma: no cover
@public
class KeyBuildEnum(IntEnum): # pragma: no cover
+ """ECTester's key builder type."""
BUILD_KEYPAIR = 0x01
BUILD_KEYBUILDER = 0x02
@public
class ExportEnum(IntEnum): # pragma: no cover
+ """ECTester's export boolean."""
EXPORT_TRUE = 0xff
EXPORT_FALSE = 0x00
@@ -90,12 +97,14 @@ class ExportEnum(IntEnum): # pragma: no cover
@public
class RunModeEnum(IntEnum): # pragma: no cover
+ """ECTester's run mode."""
MODE_NORMAL = 0xaa
MODE_DRY_RUN = 0xbb
@public
class KeyEnum(ShiftableFlag): # pragma: no cover
+ """ECTester's key enum."""
PUBLIC = 0x01
PRIVATE = 0x02
BOTH = PRIVATE | PUBLIC
@@ -103,18 +112,21 @@ class KeyEnum(ShiftableFlag): # pragma: no cover
@public
class AppletBaseEnum(IntEnum): # pragma: no cover
+ """ECTester's JavaCard applet base version."""
BASE_221 = 0x0221
BASE_222 = 0x0222
@public
class KeyClassEnum(IntEnum): # pragma: no cover
+ """JavaCard EC-based key class."""
ALG_EC_F2M = 4
ALG_EC_FP = 5
@public
class KeyAgreementEnum(IntEnum): # pragma: no cover
+ """JavaCard `KeyAgreement` type values."""
ALG_EC_SVDP_DH = 1
ALG_EC_SVDP_DH_KDF = 1
ALG_EC_SVDP_DHC = 2
@@ -127,6 +139,7 @@ class KeyAgreementEnum(IntEnum): # pragma: no cover
@public
class SignatureEnum(IntEnum): # pragma: no cover
+ """JavaCard `Signature` type values."""
ALG_ECDSA_SHA = 17
ALG_ECDSA_SHA_224 = 37
ALG_ECDSA_SHA_256 = 33
@@ -136,6 +149,7 @@ class SignatureEnum(IntEnum): # pragma: no cover
@public
class TransformationEnum(ShiftableFlag): # pragma: no cover
+ """ECTester's point/value transformation types."""
NONE = 0x00
FIXED = 0x01
FULLRANDOM = 0x02
@@ -152,6 +166,7 @@ class TransformationEnum(ShiftableFlag): # pragma: no cover
@public
class FormatEnum(IntEnum): # pragma: no cover
+ """ECTester's point format types."""
UNCOMPRESSED = 0
COMPRESSED = 1
HYBRID = 2
@@ -159,6 +174,7 @@ class FormatEnum(IntEnum): # pragma: no cover
@public
class CurveEnum(IntEnum): # pragma: no cover
+ """ECTester's curve constants."""
default = 0x00
external = 0xff
secp112r1 = 0x01
@@ -178,6 +194,7 @@ class CurveEnum(IntEnum): # pragma: no cover
@public
class ParameterEnum(ShiftableFlag): # pragma: no cover
+ """ECTester's parameter ids."""
NONE = 0x00
FP = 0x01
F2M = 0x02
@@ -196,10 +213,12 @@ class ParameterEnum(ShiftableFlag): # pragma: no cover
@public
class ChunkingException(Exception): # pragma: no cover
+ """An exception that is raised if an error happened during the chunking process of a large APDU."""
pass
class Response(ABC): # pragma: no cover
+ """An abstract base class of a response APDU."""
resp: ResponseAPDU
sws: List[int]
params: List[bytes]
diff --git a/pyecsca/sca/target/flash.py b/pyecsca/sca/target/flash.py
index bfdb79f..d1fc16a 100644
--- a/pyecsca/sca/target/flash.py
+++ b/pyecsca/sca/target/flash.py
@@ -1,3 +1,6 @@
+"""
+This module provides a mix-in class of a flashable target (e.g. one where the code gets flashed to it before running).
+"""
from public import public
from abc import ABC, abstractmethod
@@ -8,4 +11,10 @@ class Flashable(ABC):
@abstractmethod
def flash(self, fw_path: str) -> bool:
+ """
+ Flash the firmware at `fw_path` to the target.
+
+ :param fw_path: The path to the firmware blob.
+ :return: Whether the flashing was successful.
+ """
...
diff --git a/pyecsca/sca/target/serial.py b/pyecsca/sca/target/serial.py
index ccde326..d0e276c 100644
--- a/pyecsca/sca/target/serial.py
+++ b/pyecsca/sca/target/serial.py
@@ -1,5 +1,7 @@
+"""
+This module provides an abstract serial target, that communicates by writing and reading a stream of bytes.
+"""
from abc import abstractmethod
-from typing import Optional
from public import public
@@ -8,11 +10,24 @@ from .base import Target
@public
class SerialTarget(Target):
+ """A serial target."""
@abstractmethod
- def write(self, data: bytes):
+ def write(self, data: bytes) -> None:
+ """
+ Write the `data` bytes to the target's serial input.
+
+ :param data: The data to write.
+ """
...
@abstractmethod
def read(self, num: int = 0, timeout: int = 0) -> bytes:
+ """
+ Read upto `num` bytes or until `timeout` milliseconds from the target's serial output.
+
+ :param num: The number of bytes to read, `0` for all available.
+ :param timeout: The timeout in milliseconds.
+ :return: The bytes read.
+ """
...
diff --git a/pyecsca/sca/target/simpleserial.py b/pyecsca/sca/target/simpleserial.py
index 1ad0b68..03e5768 100644
--- a/pyecsca/sca/target/simpleserial.py
+++ b/pyecsca/sca/target/simpleserial.py
@@ -1,3 +1,7 @@
+"""
+This module provides an abstract target class communicating using the
+`ChipWhisperer's <https://github.com/newaetech/chipwhisperer/>`_ SimpleSerial protocol.
+"""
from time import time_ns, sleep
from typing import Mapping, Union
@@ -8,6 +12,7 @@ from .serial import SerialTarget
@public
class SimpleSerialMessage(object):
+ """A SimpleSerial message consisting of a starting character and a hexadecimal string."""
char: str
data: str
@@ -33,6 +38,7 @@ class SimpleSerialMessage(object):
@public
class SimpleSerialTarget(SerialTarget):
+ """A SimpleSerial target, sends and receives SimpleSerial messages over a serial link."""
def recv_msgs(self, timeout: int) -> Mapping[str, SimpleSerialMessage]:
start = time_ns() // 1000000
@@ -56,10 +62,12 @@ class SimpleSerialTarget(SerialTarget):
def send_cmd(self, cmd: SimpleSerialMessage, timeout: int) -> Mapping[str, SimpleSerialMessage]:
"""
+ Send a :py:class:`SimpleSerialMessage` and receive the response messages that the command produces,
+ within a `timeout`.
- :param cmd:
- :param timeout:
- :return:
+ :param cmd: The command message to send.
+ :param timeout: The timeout value to wait for the responses.
+ :return: A mapping of the starting character of the message to the message.
"""
data = bytes(cmd)
for i in range(0, len(data), 64):
diff --git a/pyecsca/sca/trace/align.py b/pyecsca/sca/trace/align.py
index e35025d..56be5b3 100644
--- a/pyecsca/sca/trace/align.py
+++ b/pyecsca/sca/trace/align.py
@@ -11,8 +11,8 @@ from .process import normalize
from .trace import Trace
-def align_reference(reference: Trace, *traces: Trace,
- align_func: Callable[[Trace], Tuple[bool, int]]) -> Tuple[List[Trace], List[int]]:
+def _align_reference(reference: Trace, *traces: Trace,
+ align_func: Callable[[Trace], Tuple[bool, int]]) -> Tuple[List[Trace], List[int]]:
result = [deepcopy(reference)]
offsets = [0]
for trace in traces:
@@ -72,7 +72,7 @@ def align_correlation(reference: Trace, *traces: Trace,
shift = left_space + reference_length // 2
return True, max_correlation_offset - shift
- return align_reference(reference, *traces, align_func=align_func)
+ return _align_reference(reference, *traces, align_func=align_func)
@public
@@ -102,7 +102,7 @@ def align_peaks(reference: Trace, *traces: Trace,
left_space = min(max_offset, reference_offset)
return True, int(window_peak - reference_peak - left_space)
- return align_reference(reference, *traces, align_func=align_func)
+ return _align_reference(reference, *traces, align_func=align_func)
@public
@@ -142,7 +142,7 @@ def align_offset(reference: Trace, *traces: Trace,
return True, best_offset
else:
return False, 0
- return align_reference(reference, *traces, align_func=align_func)
+ return _align_reference(reference, *traces, align_func=align_func)
@public
diff --git a/pyecsca/sca/trace/combine.py b/pyecsca/sca/trace/combine.py
index d73470a..8b80869 100644
--- a/pyecsca/sca/trace/combine.py
+++ b/pyecsca/sca/trace/combine.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for combining traces sample-wise.
+"""
from typing import Callable, Optional, Tuple
import numpy as np
diff --git a/pyecsca/sca/trace/edit.py b/pyecsca/sca/trace/edit.py
index cc27dcd..c0af078 100644
--- a/pyecsca/sca/trace/edit.py
+++ b/pyecsca/sca/trace/edit.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for editing traces as if they were tapes you can trim, reverse, etc.
+"""
import numpy as np
from public import public
from typing import Union, Tuple, Any
diff --git a/pyecsca/sca/trace/filter.py b/pyecsca/sca/trace/filter.py
index cb5dde6..720d311 100644
--- a/pyecsca/sca/trace/filter.py
+++ b/pyecsca/sca/trace/filter.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for filtering traces using digital (low/high/band)-pass and bandstop filters.
+"""
from public import public
from scipy.signal import butter, lfilter
from typing import Union, Tuple
@@ -5,8 +8,8 @@ from typing import Union, Tuple
from .trace import Trace
-def filter_any(trace: Trace, sampling_frequency: int,
- cutoff: Union[int, Tuple[int, int]], band_type: str) -> Trace:
+def _filter_any(trace: Trace, sampling_frequency: int,
+ cutoff: Union[int, Tuple[int, int]], band_type: str) -> Trace:
nyq = 0.5 * sampling_frequency
if not isinstance(cutoff, int):
b, a = butter(6, tuple(map(lambda x: x / nyq, cutoff)), btype=band_type, analog=False, output='ba')
@@ -26,7 +29,7 @@ def filter_lowpass(trace: Trace, sampling_frequency: int, cutoff: int) -> Trace:
:param cutoff:
:return:
"""
- return filter_any(trace, sampling_frequency, cutoff, "lowpass")
+ return _filter_any(trace, sampling_frequency, cutoff, "lowpass")
@public
@@ -40,7 +43,7 @@ def filter_highpass(trace: Trace, sampling_frequency: int, cutoff: int) -> Trace
:param cutoff:
:return:
"""
- return filter_any(trace, sampling_frequency, cutoff, "highpass")
+ return _filter_any(trace, sampling_frequency, cutoff, "highpass")
@public
@@ -55,7 +58,7 @@ def filter_bandpass(trace: Trace, sampling_frequency: int, low: int, high: int)
:param high:
:return:
"""
- return filter_any(trace, sampling_frequency, (low, high), "bandpass")
+ return _filter_any(trace, sampling_frequency, (low, high), "bandpass")
@public
@@ -70,4 +73,4 @@ def filter_bandstop(trace: Trace, sampling_frequency: int, low: int, high: int)
:param high:
:return:
"""
- return filter_any(trace, sampling_frequency, (low, high), "bandstop")
+ return _filter_any(trace, sampling_frequency, (low, high), "bandstop")
diff --git a/pyecsca/sca/trace/match.py b/pyecsca/sca/trace/match.py
index 3b4daed..3728d13 100644
--- a/pyecsca/sca/trace/match.py
+++ b/pyecsca/sca/trace/match.py
@@ -13,6 +13,15 @@ from .trace import Trace
@public
def match_pattern(trace: Trace, pattern: Trace, threshold: float = 0.8) -> List[int]:
+ """
+ Match a `pattern` to a `trace`. Returns indices where the pattern matches, e.g. those where correlation
+ of the two traces has peaks larger than `threshold`. Uses the :py:func:`scipy.signal.find_peaks` function.
+
+ :param trace: The trace to match into.
+ :param pattern: The pattern to match.
+ :param threshold: The threshold passed to :py:func:`scipy.signal.find_peaks` as a `prominence` value.
+ :return: Indices where the pattern matches.
+ """
normalized = normalize(trace)
pattern_samples = normalize(pattern).samples
correlation = np.correlate(normalized.samples, pattern_samples, "same")
@@ -34,5 +43,16 @@ def match_pattern(trace: Trace, pattern: Trace, threshold: float = 0.8) -> List[
@public
-def match_part(trace: Trace, offset: int, length: int) -> List[int]:
- return match_pattern(trace, trim(trace, offset, offset + length))
+def match_part(trace: Trace, offset: int, length: int, threshold: float = 0.8) -> List[int]:
+ """
+ Match a part of a `trace` starting at `offset` of `length` to the `trace`. Returns indices where the pattern matches
+ , e.g. those where correlation of the two traces has peaks larger than `threshold`. Uses the
+ :py:func:`scipy.signal.find_peaks` function.
+
+ :param trace: The trace to match into.
+ :param offset: The start of the pattern in the trace to match.
+ :param length: The length of the pattern in the trace to match.
+ :param threshold: The threshold passed to :py:func:`scipy.signal.find_peaks` as a `prominence` value.
+ :return: Indices where the part of the trace matches matches.
+ """
+ return match_pattern(trace, trim(trace, offset, offset + length), threshold)
diff --git a/pyecsca/sca/trace/process.py b/pyecsca/sca/trace/process.py
index 66dfe73..5b31ee8 100644
--- a/pyecsca/sca/trace/process.py
+++ b/pyecsca/sca/trace/process.py
@@ -1,3 +1,6 @@
+"""
+This module provides functions for sample-wise processing of single traces.
+"""
import numpy as np
from public import public
@@ -41,7 +44,7 @@ def threshold(trace: Trace, value) -> Trace:
return trace.with_samples(result_samples)
-def rolling_window(samples: np.ndarray, window: int) -> np.ndarray:
+def _rolling_window(samples: np.ndarray, window: int) -> np.ndarray:
shape = samples.shape[:-1] + (samples.shape[-1] - window + 1, window)
strides = samples.strides + (samples.strides[-1],)
return np.lib.stride_tricks.as_strided(samples, shape=shape, strides=strides)
@@ -56,7 +59,7 @@ def rolling_mean(trace: Trace, window: int) -> Trace:
:param window:
:return:
"""
- return trace.with_samples(np.mean(rolling_window(trace.samples, window), -1).astype(
+ return trace.with_samples(np.mean(_rolling_window(trace.samples, window), -1).astype(
dtype=trace.samples.dtype, copy=False))
@@ -72,7 +75,7 @@ def offset(trace: Trace, offset) -> Trace:
return trace.with_samples(trace.samples + offset)
-def root_mean_square(trace: Trace):
+def _root_mean_square(trace: Trace):
return np.sqrt(np.mean(np.square(trace.samples)))
@@ -84,16 +87,28 @@ def recenter(trace: Trace) -> Trace:
:param trace:
:return:
"""
- around = root_mean_square(trace)
+ around = _root_mean_square(trace)
return offset(trace, -around)
@public
def normalize(trace: Trace) -> Trace:
+ """
+ Normalize a `trace` by subtracting its mean and dividing by its standard deviation.
+
+ :param trace:
+ :return:
+ """
return trace.with_samples((trace.samples - np.mean(trace.samples)) / np.std(trace.samples))
@public
def normalize_wl(trace: Trace) -> Trace:
+ """
+ Normalize a `trace` by subtracting its mean and dividing by a multiple (= `len(trace)`) of its standard deviation.
+
+ :param trace:
+ :return:
+ """
return trace.with_samples((trace.samples - np.mean(trace.samples)) / (
np.std(trace.samples) * len(trace.samples)))
diff --git a/pyecsca/sca/trace/sampling.py b/pyecsca/sca/trace/sampling.py
index b5b7afb..9dc6bf5 100644
--- a/pyecsca/sca/trace/sampling.py
+++ b/pyecsca/sca/trace/sampling.py
@@ -1,3 +1,6 @@
+"""
+This module provides downsampling functions for traces.
+"""
import numpy as np
from public import public
from scipy.signal import decimate
diff --git a/pyecsca/sca/trace/test.py b/pyecsca/sca/trace/test.py
index 247e658..8512a70 100644
--- a/pyecsca/sca/trace/test.py
+++ b/pyecsca/sca/trace/test.py
@@ -1,3 +1,6 @@
+"""
+This module provides statistical tests usable on groups of traces sample-wise (Welch's and Student's t-test, ...).
+"""
from typing import Sequence, Optional, Tuple
import numpy as np
@@ -9,8 +12,8 @@ from .combine import average_and_variance
from .edit import trim
-def ttest_func(first_set: Sequence[Trace], second_set: Sequence[Trace],
- equal_var: bool) -> Optional[CombinedTrace]:
+def _ttest_func(first_set: Sequence[Trace], second_set: Sequence[Trace],
+ equal_var: bool) -> Optional[CombinedTrace]:
if not first_set or not second_set or len(first_set) == 0 or len(second_set) == 0:
return None
first_stack = np.stack([first.samples for first in first_set])
@@ -73,7 +76,7 @@ def student_ttest(first_set: Sequence[Trace], second_set: Sequence[Trace]) -> Op
:param second_set:
:return: Student's t-values (samplewise)
"""
- return ttest_func(first_set, second_set, True)
+ return _ttest_func(first_set, second_set, True)
@public
diff --git a/pyecsca/sca/trace/trace.py b/pyecsca/sca/trace/trace.py
index 764dada..aef0837 100644
--- a/pyecsca/sca/trace/trace.py
+++ b/pyecsca/sca/trace/trace.py
@@ -1,3 +1,6 @@
+"""
+This module provides the Trace class.
+"""
import weakref
from typing import Any, Mapping, Sequence
from copy import copy, deepcopy
@@ -14,6 +17,13 @@ class Trace(object):
samples: ndarray
def __init__(self, samples: ndarray, meta: Mapping[str, Any] = None, trace_set: Any = None):
+ """
+ Construct a new trace.
+
+ :param samples: The sample array of the trace.
+ :param meta: Metadata associated with the trace.
+ :param trace_set: A trace set the trace is contained in.
+ """
if meta is None:
meta = {}
self.meta = meta
@@ -38,12 +48,18 @@ class Trace(object):
@property
def trace_set(self) -> Any:
+ """
+ The trace set this trace is contained in, if any.
+ """
if self._trace_set is None:
return None
return self._trace_set()
@trace_set.setter
def trace_set(self, trace_set: Any):
+ """
+ Set the trace set of this trace.
+ """
if trace_set is None:
self._trace_set = None
else:
@@ -64,6 +80,12 @@ class Trace(object):
return np.array_equal(self.samples, other.samples) and self.meta == other.meta
def with_samples(self, samples: ndarray) -> "Trace":
+ """
+ Construct a copy of this trace, with the same metadata, but samples replaced by `samples`.
+
+ :param samples: The samples of the new trace.
+ :return: The new trace.
+ """
return Trace(samples, deepcopy(self.meta))
def __copy__(self):
diff --git a/pyecsca/sca/trace_set/base.py b/pyecsca/sca/trace_set/base.py
index 7cd80b6..6f4e4b0 100644
--- a/pyecsca/sca/trace_set/base.py
+++ b/pyecsca/sca/trace_set/base.py
@@ -1,4 +1,6 @@
-from io import RawIOBase, BufferedIOBase
+"""
+This module provides a base traceset class.
+"""
from pathlib import Path
from typing import List, Union, BinaryIO
@@ -9,6 +11,7 @@ from ..trace import Trace
@public
class TraceSet(object):
+ """A set of traces with some metadata."""
_traces: List[Trace]
_keys: List
diff --git a/pyecsca/sca/trace_set/chipwhisperer.py b/pyecsca/sca/trace_set/chipwhisperer.py
index 05606ee..ba78a85 100644
--- a/pyecsca/sca/trace_set/chipwhisperer.py
+++ b/pyecsca/sca/trace_set/chipwhisperer.py
@@ -1,5 +1,4 @@
from configparser import ConfigParser
-from io import RawIOBase, BufferedIOBase
from itertools import zip_longest
from os.path import exists, isfile, join, basename, dirname
from pathlib import Path
@@ -17,8 +16,7 @@ class ChipWhispererTraceSet(TraceSet):
"""ChipWhisperer trace set (native) format."""
@classmethod
- def read(cls,
- input: Union[str, Path, bytes, BinaryIO]) -> "ChipWhispererTraceSet":
+ def read(cls, input: Union[str, Path, bytes, BinaryIO]) -> "ChipWhispererTraceSet":
if isinstance(input, (str, Path)):
traces, kwargs = ChipWhispererTraceSet.__read(input)
return ChipWhispererTraceSet(*traces, **kwargs)
diff --git a/pyecsca/sca/trace_set/hdf5.py b/pyecsca/sca/trace_set/hdf5.py
index 08dedeb..4b3b7b0 100644
--- a/pyecsca/sca/trace_set/hdf5.py
+++ b/pyecsca/sca/trace_set/hdf5.py
@@ -1,3 +1,8 @@
+"""
+This module provides a traceset implemented on top of the Hierarchical Data Format (HDF5). This traceset
+can be loaded "inplace" which means that it is not fully loaded into memory, and only parts of traces that
+are operated on are in memory. This is very useful for working with huge sets of traces that do not fit in memory.
+"""
import pickle
import uuid
from collections import MutableMapping
@@ -16,6 +21,7 @@ from .. import Trace
@public
class HDF5Meta(MutableMapping):
+ """Metadata mapping that is HDF5-compatible (items are picklable)."""
_dataset: h5py.AttributeManager
def __init__(self, attrs: h5py.AttributeManager):
@@ -48,6 +54,7 @@ class HDF5Meta(MutableMapping):
@public
class HDF5TraceSet(TraceSet):
+ """A traceset based on the HDF5 (Hierarchical Data Format)."""
_file: Optional[h5py.File]
_ordering: List[str]
# _meta: Optional[HDF5Meta]
diff --git a/pyecsca/sca/trace_set/inspector.py b/pyecsca/sca/trace_set/inspector.py
index 4fabb9c..cc3f81f 100644
--- a/pyecsca/sca/trace_set/inspector.py
+++ b/pyecsca/sca/trace_set/inspector.py
@@ -1,3 +1,6 @@
+"""
+This module provides a traceset implementation based on Riscure's Inspector traceset format (`.trs`).
+"""
import struct
from enum import IntEnum
from io import BytesIO, RawIOBase, BufferedIOBase, UnsupportedOperation
diff --git a/pyecsca/sca/trace_set/pickle.py b/pyecsca/sca/trace_set/pickle.py
index 29b5dd2..6abe88a 100644
--- a/pyecsca/sca/trace_set/pickle.py
+++ b/pyecsca/sca/trace_set/pickle.py
@@ -1,3 +1,7 @@
+"""
+This module provides a traceset implementation based on Python's pickle format. This implementation of the
+traceset is thus very generic.
+"""
import pickle
from io import BufferedIOBase, RawIOBase
from pathlib import Path
@@ -10,6 +14,8 @@ from .base import TraceSet
@public
class PickleTraceSet(TraceSet):
+ """Pickle-based traceset format."""
+
@classmethod
def read(cls, input: Union[str, Path, bytes, BinaryIO]) -> "PickleTraceSet":
if isinstance(input, bytes):
diff --git a/setup.py b/setup.py
index 99a4e93..fa90ccd 100644
--- a/setup.py
+++ b/setup.py
@@ -49,6 +49,7 @@ setup(
"smartcard": ["pyscard"],
"gmp": ["gmpy2"],
"dev": ["mypy", "flake8", "interrogate"],
- "test": ["nose2", "parameterized", "coverage"]
+ "test": ["nose2", "parameterized", "coverage"],
+ "doc": ["sphinx", "sphinx-autodoc-typehints", "nbsphinx"]
}
)