diff options
| author | J08nY | 2021-01-20 20:24:11 +0100 |
|---|---|---|
| committer | J08nY | 2021-01-20 20:24:11 +0100 |
| commit | 9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3 (patch) | |
| tree | bb695e485808e4d0517d84053019e2f7ddb03679 | |
| parent | adc3cd52147f35e0a7cc9008ac96619dd89cda48 (diff) | |
| download | pyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.tar.gz pyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.tar.zst pyecsca-9ec68bdb56882777e5b3670380bf1e1ad7d0a7a3.zip | |
51 files changed, 462 insertions, 78 deletions
@@ -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): @@ -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"] } ) |
