Source code for keri.core.coring

# -*- encoding: utf-8 -*-
"""
keri.core.coring module

"""
import json
from collections import namedtuple
from collections.abc import Sequence, Mapping
from dataclasses import dataclass, astuple, asdict
from base64 import urlsafe_b64encode as encodeB64
from base64 import urlsafe_b64decode as decodeB64
from fractions import Fraction

import cbor2 as cbor
import msgpack
import pysodium
import blake3
import hashlib

from cryptography import exceptions
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, utils

from ..kering import (EmptyMaterialError, RawMaterialError, SoftMaterialError,
                      InvalidCodeError, InvalidSoftError, InvalidCodeSizeError,
                      InvalidVarRawSizeError, ConversionError, InvalidValueError,
                      ValidationError, VersionError, ShortageError,
                      UnexpectedCodeError, DeserializeError, Versionage,
                      UnexpectedCountCodeError, UnexpectedOpCodeError,
                      versify, deversify, smell, MAXVERFULLSPAN,
                      Version, Vrsn_2_0, Rever, MaxON,
                      Kinds, Protocols, Ilks, TraitDex)

from ..help import (sceil, isNonStringIterable, isNonStringSequence,
                    intToB64, b64ToInt, codeB64ToB2, nabSextets,
                    codeB2ToB64, Reb64, Reatt, Repath,
                    nowIso8601, fromIso8601)

DSS_SIG_MODE = "fips-186-3"
ECDSA_256r1_SEEDBYTES = 32
ECDSA_256k1_SEEDBYTES = 32

# digest algorithm  klas, digest size (not default), digest length
# size and length are needed for some digest types as function parameters
Digestage = namedtuple("Digestage", "klas size length")

# SAID field labels
Saidage = namedtuple("Saidage", "dollar at id_ i d")

Saids = Saidage(dollar="$id", at="@id", id_="id", i="i", d="d")

[docs] def sizeify(ked, kind=None, version=Version): """ Compute serialized size of ked and update version field Returns tuple of associated values extracted and or changed by sizeify Returns tuple of (raw, proto, kind, ked, version) where: raw (str): serialized event as bytes of kind proto (str): protocol type as value of Protocolage kind (str): serialzation kind as value of Serialage ked (dict): key event dict version (Versionage): instance Parameters: ked (dict): key event dict kind (str): value of Serials is serialization type if not provided use that given in ked["v"] version (Versionage): instance supported protocol version for message Assumes only supports Version """ if "v" not in ked: raise ValueError("Missing or empty version string in key event " "dict = {}".format(ked)) proto, vrsn, knd, size, _ = deversify(ked["v"]) # extract kind and version if vrsn != version: raise ValueError("Unsupported version = {}.{}".format(vrsn.major, vrsn.minor)) if not kind: kind = knd if kind not in Kinds: raise ValueError("Invalid serialization kind = {}".format(kind)) raw = dumps(ked, kind) size = len(raw) match = Rever.search(raw) # Rever's regex takes bytes if not match or match.start() > 12: raise ValueError("Invalid version string in raw = {}".format(raw)) fore, back = match.span() # full version string # update vs with latest kind version size vs = versify(proto=proto, pvrsn=vrsn, kind=kind, size=size) # replace old version string in raw with new one raw = b'%b%b%b' % (raw[:fore], vs.encode("utf-8"), raw[back:]) if size != len(raw): # substitution messed up raise ValueError("Malformed version string size = {}".format(vs)) ked["v"] = vs # update ked return raw, proto, kind, ked, vrsn
[docs] def dumps(ked, kind=Kinds.json): """ utility function to handle serialization by kind Returns: raw (bytes): serialized version of ked dict Parameters: ked (Optional(dict, list)): key event dict or message dict to serialize kind (str): serialization kind (JSON, MGPK, CBOR) """ if kind == Kinds.json: raw = json.dumps(ked, separators=(",", ":"), ensure_ascii=False).encode("utf-8") elif kind == Kinds.mgpk: raw = msgpack.dumps(ked) elif kind == Kinds.cbor: raw = cbor.dumps(ked) else: raise ValueError("Invalid serialization kind = {}".format(kind)) return raw
[docs] def loads(raw, size=None, kind=Kinds.json): """ utility function to handle deserialization by kind Returns: ked (dict): deserialized Parameters: raw (Union[bytes,bytearray]): raw serialization to deserialze as dict size (int): number of bytes to consume for the deserialization. If None then consume all bytes kind (str): serialization kind (JSON, MGPK, CBOR) """ if kind == Kinds.json: try: ked = json.loads(raw[:size].decode("utf-8")) except Exception as ex: raise DeserializeError("Error deserializing JSON: {}" "".format(raw[:size].decode("utf-8"))) elif kind == Kinds.mgpk: try: ked = msgpack.loads(raw[:size]) except Exception as ex: raise DeserializeError("Error deserializing MGPK: {}" "".format(raw[:size])) elif kind == Kinds.cbor: try: ked = cbor.loads(raw[:size]) except Exception as ex: raise DeserializeError("Error deserializing CBOR: {}" "".format(raw[:size])) else: raise DeserializeError("Invalid deserialization kind: {}" "".format(kind)) return ked
[docs] @dataclass class MapDom: """Base class for mutable dataclasses that support map syntax Adds support for dunder methods for map syntax dc[name]. Converts exceptions from attribute syntax to raise map syntax when using map syntax. Enables dataclass instances to use Mapping item syntax """ def __getitem__(self, name): try: return getattr(self, name) except AttributeError as ex: raise IndexError(ex.args) from ex def __setitem__(self, name, value): try: return setattr(self, name, value) except AttributeError as ex: raise IndexError(ex.args) from ex def __delitem__(self, name): try: return delattr(self, name) except AttributeError as ex: raise IndexError(ex.args) from ex
[docs] @dataclass(frozen=True) class IceMapDom: """Base class for frozen dataclasses (such as codexes) that support map syntax Adds support for dunder methods for map syntax dc[name]. Converts exceptions from attribute syntax to raise map syntax when using map syntax. Enables frozen dataclass instances to use Mapping item syntax """ def __getitem__(self, name): try: return getattr(self, name) except AttributeError as ex: raise IndexError(ex.args) from ex def __setitem__(self, name, value): try: return setattr(self, name, value) except AttributeError as ex: raise IndexError(ex.args) from ex def __delitem__(self, name): try: return delattr(self, name) except AttributeError as ex: raise IndexError(ex.args) from ex
[docs] @dataclass(frozen=True) class MatterCodex: """ MatterCodex is codex code (stable) part of all matter derivation codes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Ed25519_Seed: str = 'A' # Ed25519 256 bit random seed for private key Ed25519N: str = 'B' # Ed25519 verification key non-transferable, basic derivation. X25519: str = 'C' # X25519 public encryption key, may be converted from Ed25519 or Ed25519N. Ed25519: str = 'D' # Ed25519 verification key basic derivation Blake3_256: str = 'E' # Blake3 256 bit digest self-addressing derivation. Blake2b_256: str = 'F' # Blake2b 256 bit digest self-addressing derivation. Blake2s_256: str = 'G' # Blake2s 256 bit digest self-addressing derivation. SHA3_256: str = 'H' # SHA3 256 bit digest self-addressing derivation. SHA2_256: str = 'I' # SHA2 256 bit digest self-addressing derivation. ECDSA_256k1_Seed: str = 'J' # ECDSA secp256k1 256 bit random Seed for private key Ed448_Seed: str = 'K' # Ed448 448 bit random Seed for private key X448: str = 'L' # X448 public encryption key, converted from Ed448 Short: str = 'M' # Short 2 byte b2 number Big: str = 'N' # Big 8 byte b2 number X25519_Private: str = 'O' # X25519 private decryption key/seed, may be converted from Ed25519 X25519_Cipher_Seed: str = 'P' # X25519 sealed box 124 char qb64 Cipher of 44 char qb64 Seed ECDSA_256r1_Seed: str = 'Q' # ECDSA secp256r1 256 bit random Seed for private key Tall: str = 'R' # Tall 5 byte b2 number Large: str = 'S' # Large 11 byte b2 number Great: str = 'T' # Great 14 byte b2 number Vast: str = 'U' # Vast 17 byte b2 number Label1: str = 'V' # Label1 1 bytes for label lead size 1 Label2: str = 'W' # Label2 2 bytes for label lead size 0 Tag3: str = 'X' # Tag3 3 B64 encoded chars for special values Tag7: str = 'Y' # Tag7 7 B64 encoded chars for special values Tag11: str = 'Z' # Tag11 11 B64 encoded chars for special values Salt_256: str = 'a' # Salt/seed/nonce/blind 256 bits Salt_128: str = '0A' # Salt/seed/nonce 128 bits or number of length 128 bits (Huge) Ed25519_Sig: str = '0B' # Ed25519 signature. ECDSA_256k1_Sig: str = '0C' # ECDSA secp256k1 signature. Blake3_512: str = '0D' # Blake3 512 bit digest self-addressing derivation. Blake2b_512: str = '0E' # Blake2b 512 bit digest self-addressing derivation. SHA3_512: str = '0F' # SHA3 512 bit digest self-addressing derivation. SHA2_512: str = '0G' # SHA2 512 bit digest self-addressing derivation. Long: str = '0H' # Long 4 byte b2 number ECDSA_256r1_Sig: str = '0I' # ECDSA secp256r1 signature. Tag1: str = '0J' # Tag1 1 B64 encoded char + 1 prepad for special values Tag2: str = '0K' # Tag2 2 B64 encoded chars for for special values Tag5: str = '0L' # Tag5 5 B64 encoded chars + 1 prepad for special values Tag6: str = '0M' # Tag6 6 B64 encoded chars for special values Tag9: str = '0N' # Tag9 9 B64 encoded chars + 1 prepad for special values Tag10: str = '0O' # Tag10 10 B64 encoded chars for special values GramHeadNeck: str = '0P' # GramHeadNeck 32 B64 chars memogram head with neck GramHead: str = '0Q' # GramHead 28 B64 chars memogram head only GramHeadAIDNeck: str = '0R' # GramHeadAIDNeck 76 B64 chars memogram head with AID and neck GramHeadAID: str = '0S' # GramHeadAID 72 B64 chars memogram head with AID only ECDSA_256k1N: str = '1AAA' # ECDSA secp256k1 verification key non-transferable, basic derivation. ECDSA_256k1: str = '1AAB' # ECDSA public verification or encryption key, basic derivation Ed448N: str = '1AAC' # Ed448 non-transferable prefix public signing verification key. Basic derivation. Ed448: str = '1AAD' # Ed448 public signing verification key. Basic derivation. Ed448_Sig: str = '1AAE' # Ed448 signature. Self-signing derivation. Tag4: str = '1AAF' # Tag4 4 B64 encoded chars for special values DateTime: str = '1AAG' # Base64 custom encoded 32 char ISO-8601 DateTime X25519_Cipher_Salt: str = '1AAH' # X25519 sealed box 100 char qb64 Cipher of 24 char qb64 Salt ECDSA_256r1N: str = '1AAI' # ECDSA secp256r1 verification key non-transferable, basic derivation. ECDSA_256r1: str = '1AAJ' # ECDSA secp256r1 verification or encryption key, basic derivation Null: str = '1AAK' # Null None or empty value No: str = '1AAL' # No Falsey Boolean value Yes: str = '1AAM' # Yes Truthy Boolean value Tag8: str = '1AAN' # Tag8 8 B64 encoded chars for special values Escape: str = '1AAO' # Escape code for escaping special map fields Empty: str = '1AAP' # Empty value for Nonce, UUID, or related fields TBD0S: str = '1__-' # Testing purposes only, fixed special values with non-empty raw lead size 0 TBD0: str = '1___' # Testing purposes only, fixed with lead size 0 TBD1S: str = '2__-' # Testing purposes only, fixed special values with non-empty raw lead size 1 TBD1: str = '2___' # Testing purposes only, fixed with lead size 1 TBD2S: str = '3__-' # Testing purposes only, fixed special values with non-empty raw lead size 2 TBD2: str = '3___' # Testing purposes only, fixed with lead size 2 StrB64_L0: str = '4A' # String Base64 only lead size 0 StrB64_L1: str = '5A' # String Base64 only lead size 1 StrB64_L2: str = '6A' # String Base64 only lead size 2 StrB64_Big_L0: str = '7AAA' # String Base64 only big lead size 0 StrB64_Big_L1: str = '8AAA' # String Base64 only big lead size 1 StrB64_Big_L2: str = '9AAA' # String Base64 only big lead size 2 Bytes_L0: str = '4B' # Byte String lead size 0 Bytes_L1: str = '5B' # Byte String lead size 1 Bytes_L2: str = '6B' # Byte String lead size 2 Bytes_Big_L0: str = '7AAB' # Byte String big lead size 0 Bytes_Big_L1: str = '8AAB' # Byte String big lead size 1 Bytes_Big_L2: str = '9AAB' # Byte String big lead size 2 X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 0 X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 1 X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable stream plaintext lead size 2 X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 0 X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 1 X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable stream plaintext big lead size 2 X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 X25519_Cipher_QB64_L1: str = '5D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 1 X25519_Cipher_QB64_L2: str = '6D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 2 X25519_Cipher_QB64_Big_L0: str = '7AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 0 X25519_Cipher_QB64_Big_L1: str = '8AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 1 X25519_Cipher_QB64_Big_L2: str = '9AAD' # X25519 sealed box cipher bytes of QB64 plaintext big lead size 2 X25519_Cipher_QB2_L0: str = '4E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 0 X25519_Cipher_QB2_L1: str = '5E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 1 X25519_Cipher_QB2_L2: str = '6E' # X25519 sealed box cipher bytes of QB2 plaintext lead size 2 X25519_Cipher_QB2_Big_L0: str = '7AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 0 X25519_Cipher_QB2_Big_L1: str = '8AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 1 X25519_Cipher_QB2_Big_L2: str = '9AAE' # X25519 sealed box cipher bytes of QB2 plaintext big lead size 2 HPKEBase_Cipher_L0: str = '4F' # HPKE Base cipher bytes of sniffable stream plaintext lead size 0 HPKEBase_Cipher_L1: str = '5F' # HPKE Base cipher bytes of sniffable stream plaintext lead size 1 HPKEBase_Cipher_L2: str = '6F' # HPKE Base cipher bytes of sniffable stream plaintext lead size 2 HPKEBase_Cipher_Big_L0: str = '7AAF' # HPKE Base cipher bytes of sniffable stream plaintext big lead size 0 HPKEBase_Cipher_Big_L1: str = '8AAF' # HPKE Base cipher bytes of sniffable stream plaintext big lead size 1 HPKEBase_Cipher_Big_L2: str = '9AAF' # HPKE Base cipher bytes of sniffable stream plaintext big lead size 2 Decimal_L0: str = '4H' # Decimal B64 string float and int lead size 0 Decimal_L1: str = '5H' # Decimal B64 string float and int lead size 1 Decimal_L2: str = '6H' # Decimal B64 string float and intlead size 2 Decimal_Big_L0: str = '7AAH' # Decimal B64 string float and int big lead size 0 Decimal_Big_L1: str = '8AAH' # Decimal B64 string float and int big lead size 1 Decimal_Big_L2: str = '9AAH' # Decimal B64 string float and int big lead size 2 def __iter__(self): return iter(astuple(self)) # enables inclusion test with "in"
MtrDex = MatterCodex() # Make instance
[docs] @dataclass(frozen=True) class SmallVarRawSizeCodex: """ SmallVarRawSizeCodex is codex all selector characters for the three small variable raw size tables that act as one table but with different leader byte sizes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Lead0: str = '4' # First Selector Character for all ls == 0 codes Lead1: str = '5' # First Selector Character for all ls == 1 codes Lead2: str = '6' # First Selector Character for all ls == 2 codes def __iter__(self): return iter(astuple(self))
SmallVrzDex = SmallVarRawSizeCodex() # Make instance
[docs] @dataclass(frozen=True) class LargeVarRawSizeCodex: """ LargeVarRawSizeCodex is codex all selector characters for the three large variable raw size tables that act as one table but with different leader byte sizes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Lead0_Big: str = '7' # First Selector Character for all ls == 0 codes Lead1_Big: str = '8' # First Selector Character for all ls == 1 codes Lead2_Big: str = '9' # First Selector Character for all ls == 2 codes def __iter__(self): return iter(astuple(self))
LargeVrzDex = LargeVarRawSizeCodex() # Make instance
[docs] @dataclass(frozen=True) class BextCodex: """ BextCodex is codex of all variable sized Base64 Text (Bext) derivation codes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ StrB64_L0: str = '4A' # String Base64 Only Leader Size 0 StrB64_L1: str = '5A' # String Base64 Only Leader Size 1 StrB64_L2: str = '6A' # String Base64 Only Leader Size 2 StrB64_Big_L0: str = '7AAA' # String Base64 Only Big Leader Size 0 StrB64_Big_L1: str = '8AAA' # String Base64 Only Big Leader Size 1 StrB64_Big_L2: str = '9AAA' # String Base64 Only Big Leader Size 2 def __iter__(self): return iter(astuple(self))
BexDex = BextCodex() # Make instance
[docs] @dataclass(frozen=True) class TextCodex: """ TextCodex is codex of all variable sized byte string (Text) derivation codes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Bytes_L0: str = '4B' # Byte String lead size 0 Bytes_L1: str = '5B' # Byte String lead size 1 Bytes_L2: str = '6B' # Byte String lead size 2 Bytes_Big_L0: str = '7AAB' # Byte String big lead size 0 Bytes_Big_L1: str = '8AAB' # Byte String big lead size 1 Bytes_Big_L2: str = '9AAB' # Byte String big lead size 2 def __iter__(self): return iter(astuple(self))
TexDex = TextCodex() # Make instance
[docs] @dataclass(frozen=True) class DecimalCodex: """DecimalCodex is codex of all variable sized Base64 String representation of decimal numbers both signed and unsigned, float and int. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Decimal_L0: str = '4H' # Decimal B64 string float and int lead size 0 Decimal_L1: str = '5H' # Decimal B64string float and int lead size 1 Decimal_L2: str = '6H' # Decimal B64string float and intlead size 2 Decimal_Big_L0: str = '7AAH' # Decimal B64string float and int big lead size 0 Decimal_Big_L1: str = '8AAH' # Decimal B64string float and int big lead size 1 Decimal_Big_L2: str = '9AAH' # Decimal B64string float and int big lead size 2 def __iter__(self): return iter(astuple(self))
DecDex = DecimalCodex() # Make instance # When add new to DigCodes update Saider.Digests and Serder.Digests class attr
[docs] @dataclass(frozen=True) class DigCodex: """ DigCodex is codex all digest derivation codes. This is needed to ensure delegated inception using a self-addressing derivation i.e. digest derivation code. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Blake3_256: str = 'E' # Blake3 256 bit digest self-addressing derivation. Blake2b_256: str = 'F' # Blake2b 256 bit digest self-addressing derivation. Blake2s_256: str = 'G' # Blake2s 256 bit digest self-addressing derivation. SHA3_256: str = 'H' # SHA3 256 bit digest self-addressing derivation. SHA2_256: str = 'I' # SHA2 256 bit digest self-addressing derivation. Blake3_512: str = '0D' # Blake3 512 bit digest self-addressing derivation. Blake2b_512: str = '0E' # Blake2b 512 bit digest self-addressing derivation. SHA3_512: str = '0F' # SHA3 512 bit digest self-addressing derivation. SHA2_512: str = '0G' # SHA2 512 bit digest self-addressing derivation. def __iter__(self): return iter(astuple(self))
DigDex = DigCodex() # Make instance
[docs] @dataclass(frozen=True) class NonceCodex: """NonceCodex is codex all derivation codes for salty nonces (UUIDs) either as random numbers or as digests deterministically derived from salty nonces. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Empty: str = '1AAP' # Empty value for Nonce, UUID, or related fields Salt_128: str = '0A' # random salt/seed/nonce/private key Salt_256: str = 'a' # Salt/seed/nonce/blind 256 bits Blake3_256: str = 'E' # Blake3 256 bit digest self-addressing derivation. Blake2b_256: str = 'F' # Blake2b 256 bit digest self-addressing derivation. Blake2s_256: str = 'G' # Blake2s 256 bit digest self-addressing derivation. SHA3_256: str = 'H' # SHA3 256 bit digest self-addressing derivation. SHA2_256: str = 'I' # SHA2 256 bit digest self-addressing derivation. Blake3_512: str = '0D' # Blake3 512 bit digest self-addressing derivation. Blake2b_512: str = '0E' # Blake2b 512 bit digest self-addressing derivation. SHA3_512: str = '0F' # SHA3 512 bit digest self-addressing derivation. SHA2_512: str = '0G' # SHA2 512 bit digest self-addressing derivation. def __iter__(self): return iter(astuple(self))
NonceDex = NonceCodex() # Make instance
[docs] @dataclass(frozen=True) class NumCodex: """ NumCodex is codex of Base64 derivation codes for compactly representing numbers across a wide rage of sizes. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Short: str = 'M' # Short 2 byte b2 number Long: str = '0H' # Long 4 byte b2 number Tall: str = 'R' # Tall 5 byte b2 number Big: str = 'N' # Big 8 byte b2 number Large: str = 'S' # Large 11 byte b2 number Great: str = 'T' # Great 14 byte b2 number Huge: str = '0A' # Huge 16 byte b2 number (same as Salt_128) Vast: str = 'U' # Vast 17 byte b2 number def __iter__(self): return iter(astuple(self))
NumDex = NumCodex() # Make instance
[docs] @dataclass(frozen=True) class TagCodex: """ TagCodex is codex of Base64 derivation codes for compactly representing various small Base64 tag values as special code soft part values. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Tag1: str = '0J' # 1 B64 char tag with 1 pre pad Tag2: str = '0K' # 2 B64 char tag Tag3: str = 'X' # 3 B64 char tag Tag4: str = '1AAF' # 4 B64 char tag Tag5: str = '0L' # 5 B64 char tag with 1 pre pad Tag6: str = '0M' # 6 B64 char tag Tag7: str = 'Y' # 7 B64 char tag Tag8: str = '1AAN' # 8 B64 char tag Tag9: str = '0N' # 9 B64 char tag with 1 pre pad Tag10: str = '0O' # 10 B64 char tag Tag11: str = 'Z' # 11 B64 char tag def __iter__(self): return iter(astuple(self))
TagDex = TagCodex() # Make instance
[docs] @dataclass(frozen=True) class LabelCodex: """LabelCodex is codex of codes to compactly ser/des labels and string values in maps or lists. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Empty: str = '1AAP' # Empty value for Nonce, UUID, state or related fields Tag1: str = '0J' # 1 B64 char tag with 1 pre pad Tag2: str = '0K' # 2 B64 char tag Tag3: str = 'X' # 3 B64 char tag Tag4: str = '1AAF' # 4 B64 char tag Tag5: str = '0L' # 5 B64 char tag with 1 pre pad Tag6: str = '0M' # 6 B64 char tag Tag7: str = 'Y' # 7 B64 char tag Tag8: str = '1AAN' # 8 B64 char tag Tag9: str = '0N' # 9 B64 char tag with 1 pre pad Tag10: str = '0O' # 10 B64 char tag Tag11: str = 'Z' # 11 B64 char tag StrB64_L0: str = '4A' # String Base64 Only Leader Size 0 StrB64_L1: str = '5A' # String Base64 Only Leader Size 1 StrB64_L2: str = '6A' # String Base64 Only Leader Size 2 StrB64_Big_L0: str = '7AAA' # String Base64 Only Big Leader Size 0 StrB64_Big_L1: str = '8AAA' # String Base64 Only Big Leader Size 1 StrB64_Big_L2: str = '9AAA' # String Base64 Only Big Leader Size 2 Label1: str = 'V' # Label1 1 bytes for label lead size 1 Label2: str = 'W' # Label2 2 bytes for label lead size 0 Bytes_L0: str = '4B' # Byte String lead size 0 Bytes_L1: str = '5B' # Byte String lead size 1 Bytes_L2: str = '6B' # Byte String lead size 2 Bytes_Big_L0: str = '7AAB' # Byte String big lead size 0 Bytes_Big_L1: str = '8AAB' # Byte String big lead size 1 Bytes_Big_L2: str = '9AAB' # Byte String big lead size 2 def __iter__(self): return iter(astuple(self))
LabelDex = LabelCodex() # Make instance
[docs] @dataclass(frozen=True) class PreCodex: """ PreCodex is codex all identifier prefix derivation codes. This is needed to verify valid inception events. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Ed25519N: str = 'B' # Ed25519 verification key non-transferable, basic derivation. Ed25519: str = 'D' # Ed25519 verification key, basic derivation. Blake3_256: str = 'E' # Blake3 256 bit digest self-addressing derivation. Blake2b_256: str = 'F' # Blake2b 256 bit digest self-addressing derivation. Blake2s_256: str = 'G' # Blake2s 256 bit digest self-addressing derivation. SHA3_256: str = 'H' # SHA3 256 bit digest self-addressing derivation. SHA2_256: str = 'I' # SHA2 256 bit digest self-addressing derivation. Blake3_512: str = '0D' # Blake3 512 bit digest self-addressing derivation. Blake2b_512: str = '0E' # Blake2b 512 bit digest self-addressing derivation. SHA3_512: str = '0F' # SHA3 512 bit digest self-addressing derivation. SHA2_512: str = '0G' # SHA2 512 bit digest self-addressing derivation. ECDSA_256k1N: str = '1AAA' # ECDSA secp256k1 verification key non-transferable, basic derivation. ECDSA_256k1: str = '1AAB' # ECDSA public verification or encryption key, basic derivation Ed448N: str = '1AAC' # Ed448 verification key non-transferable, basic derivation. Ed448: str = '1AAD' # Ed448 verification key, basic derivation. Ed448_Sig: str = '1AAE' # Ed448 signature. Self-signing derivation. ECDSA_256r1N: str = "1AAI" # ECDSA secp256r1 verification key non-transferable, basic derivation. ECDSA_256r1: str = "1AAJ" # ECDSA secp256r1 verification or encryption key, basic derivation def __iter__(self): return iter(astuple(self))
PreDex = PreCodex() # Make instance
[docs] @dataclass(frozen=True) class NonTransCodex: """ NonTransCodex is codex all non-transferable derivation codes Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Ed25519N: str = 'B' # Ed25519 verification key non-transferable, basic derivation. ECDSA_256k1N: str = '1AAA' # ECDSA secp256k1 verification key non-transferable, basic derivation. Ed448N: str = '1AAC' # Ed448 verification key non-transferable, basic derivation. ECDSA_256r1N: str = "1AAI" # ECDSA secp256r1 verification key non-transferable, basic derivation. def __iter__(self): return iter(astuple(self))
NonTransDex = NonTransCodex() # Make instance
[docs] @dataclass(frozen=True) class PreNonDigCodex: """ PreNonDigCodex is codex all prefixive but non-digestive derivation codes Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ Ed25519N: str = 'B' # Ed25519 verification key non-transferable, basic derivation. Ed25519: str = 'D' # Ed25519 verification key, basic derivation. ECDSA_256k1N: str = '1AAA' # ECDSA secp256k1 verification key non-transferable, basic derivation. ECDSA_256k1: str = '1AAB' # ECDSA public verification or encryption key, basic derivation Ed448N: str = '1AAC' # Ed448 verification key non-transferable, basic derivation. Ed448: str = '1AAD' # Ed448 verification key, basic derivation. ECDSA_256r1N: str = "1AAI" # ECDSA secp256r1 verification key non-transferable, basic derivation. ECDSA_256r1: str = "1AAJ" # ECDSA secp256r1 verification or encryption key, basic derivation def __iter__(self): return iter(astuple(self))
PreNonDigDex = PreNonDigCodex() # Make instance # namedtuple for size entries in Matter and Counter derivation code tables # hs is the hard size int number of chars in hard (stable) part of code # ss is the soft size int number of chars in soft (unstable) part of code # xs is the xtra size int number of xtra (pre-pad) chars as part of soft # fs is the full size int number of chars in code plus appended material if any # ls is the lead size int number of bytes to pre-pad pre-converted raw binary Sizage = namedtuple("Sizage", "hs ss xs fs ls")
[docs] class Matter: """ Matter is fully qualified cryptographic material primitive base class for non-indexed primitives. Sub classes are derivation code and key event element context specific. Includes the following attributes and properties: Class Attributes: Codex (MatterCodex): MtrDex Hards (dict): hard sizes keyed by qb64 selector Bards (dict): hard size keyed by qb2 selector Sizes (dict): sizes tables for codes Codes (dict): maps code name to code Names (dict): maps code to code name Pad (str): B64 pad char for xtra size pre-padded soft values Class Methods: Attributes: Properties: code (str): hard part of derivation code to indicate cypher suite hard (str): hard part of derivation code. alias for code soft (str | bytes): soft part of full code exclusive of xs xtra prepad. Empty when ss = 0. both (str): hard + soft parts of full text code size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) fullSize (int): full size of primitive raw (bytes): crypto material only. Not derivation code or lead bytes. qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise prefixive (bool): True means identifier prefix derivation code False otherwise special (bool): True when soft is special raw is empty and fixed size composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip Hidden: _code (str): value for .code property _soft (str): soft value of full code _raw (bytes): value for .raw property _rawSize(): _fullSize(): _leadSize(): _special(): _infil(): creates qb64b from .raw and .code (fully qualified Base64) _binfil(): creates qb2 from .raw and .code (fully qualified Base2) _exfil(): extracts .code and .raw from qb64b (fully qualified Base64) _bexfil(): extracts .code and .raw from qb2 (fully qualified Base2) Special soft values are indicated when fn in table is None and ss > 0. """ Codex = MtrDex # class variable holding MatterDex reference # Hards table maps from bytes Base64 first code char to int of hard size, hs, # (stable) of code. The soft size, ss, (unstable) is always 0 for Matter # unless fs is None which allows for variable size multiple of 4, i.e. # not (hs + ss) % 4. Hards = ({chr(c): 1 for c in range(65, 65 + 26)}) # size of hard part of code Hards.update({chr(c): 1 for c in range(97, 97 + 26)}) Hards.update([('0', 2), ('1', 4), ('2', 4), ('3', 4), ('4', 2), ('5', 2), ('6', 2), ('7', 4), ('8', 4), ('9', 4)]) # Bards table maps first code char. converted to binary sextext of hard size, # hs. Used for ._bexfil. Bards = ({codeB64ToB2(c): hs for c, hs in Hards.items()}) # Sizes table maps from value of hs chars of code to Sizage namedtuple of # (hs, ss, xs, fs, ls) where hs is hard size, ss is soft size, # xs is extra size of soft, fs is full size, and ls is lead size of raw. Sizes = { 'A': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'B': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'C': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'D': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'E': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'F': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'G': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'H': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'I': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'J': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'K': Sizage(hs=1, ss=0, xs=0, fs=76, ls=0), 'L': Sizage(hs=1, ss=0, xs=0, fs=76, ls=0), 'M': Sizage(hs=1, ss=0, xs=0, fs=4, ls=0), 'N': Sizage(hs=1, ss=0, xs=0, fs=12, ls=0), 'O': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'P': Sizage(hs=1, ss=0, xs=0, fs=124, ls=0), 'Q': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), 'R': Sizage(hs=1, ss=0, xs=0, fs=8, ls=0), 'S': Sizage(hs=1, ss=0, xs=0, fs=16, ls=0), 'T': Sizage(hs=1, ss=0, xs=0, fs=20, ls=0), 'U': Sizage(hs=1, ss=0, xs=0, fs=24, ls=0), 'V': Sizage(hs=1, ss=0, xs=0, fs=4, ls=1), 'W': Sizage(hs=1, ss=0, xs=0, fs=4, ls=0), 'X': Sizage(hs=1, ss=3, xs=0, fs=4, ls=0), 'Y': Sizage(hs=1, ss=7, xs=0, fs=8, ls=0), 'Z': Sizage(hs=1, ss=11, xs=0, fs=12, ls=0), 'a': Sizage(hs=1, ss=0, xs=0, fs=44, ls=0), '0A': Sizage(hs=2, ss=0, xs=0, fs=24, ls=0), '0B': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0C': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0D': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0E': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0F': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0G': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0H': Sizage(hs=2, ss=0, xs=0, fs=8, ls=0), '0I': Sizage(hs=2, ss=0, xs=0, fs=88, ls=0), '0J': Sizage(hs=2, ss=2, xs=1, fs=4, ls=0), '0K': Sizage(hs=2, ss=2, xs=0, fs=4, ls=0), '0L': Sizage(hs=2, ss=6, xs=1, fs=8, ls=0), '0M': Sizage(hs=2, ss=6, xs=0, fs=8, ls=0), '0N': Sizage(hs=2, ss=10, xs=1, fs=12, ls=0), '0O': Sizage(hs=2, ss=10, xs=0, fs=12, ls=0), '0P': Sizage(hs=2, ss=22, xs=0, fs=32, ls=0), '0Q': Sizage(hs=2, ss=22, xs=0, fs=28, ls=0), '0R': Sizage(hs=2, ss=22, xs=0, fs=76, ls=0), '0S': Sizage(hs=2, ss=22, xs=0, fs=72, ls=0), '1AAA': Sizage(hs=4, ss=0, xs=0, fs=48, ls=0), '1AAB': Sizage(hs=4, ss=0, xs=0, fs=48, ls=0), '1AAC': Sizage(hs=4, ss=0, xs=0, fs=80, ls=0), '1AAD': Sizage(hs=4, ss=0, xs=0, fs=80, ls=0), '1AAE': Sizage(hs=4, ss=0, xs=0, fs=156, ls=0), '1AAF': Sizage(hs=4, ss=4, xs=0, fs=8, ls=0), '1AAG': Sizage(hs=4, ss=0, xs=0, fs=36, ls=0), '1AAH': Sizage(hs=4, ss=0, xs=0, fs=100, ls=0), '1AAI': Sizage(hs=4, ss=0, xs=0, fs=48, ls=0), '1AAJ': Sizage(hs=4, ss=0, xs=0, fs=48, ls=0), '1AAK': Sizage(hs=4, ss=0, xs=0, fs=4, ls=0), '1AAL': Sizage(hs=4, ss=0, xs=0, fs=4, ls=0), '1AAM': Sizage(hs=4, ss=0, xs=0, fs=4, ls=0), '1AAN': Sizage(hs=4, ss=8, xs=0, fs=12, ls=0), '1AAO': Sizage(hs=4, ss=0, xs=0, fs=4, ls=0), '1AAP': Sizage(hs=4, ss=0, xs=0, fs=4, ls=0), '1__-': Sizage(hs=4, ss=2, xs=0, fs=12, ls=0), '1___': Sizage(hs=4, ss=0, xs=0, fs=8, ls=0), '2__-': Sizage(hs=4, ss=2, xs=1, fs=12, ls=1), '2___': Sizage(hs=4, ss=0, xs=0, fs=8, ls=1), '3__-': Sizage(hs=4, ss=2, xs=0, fs=12, ls=2), '3___': Sizage(hs=4, ss=0, xs=0, fs=8, ls=2), '4A': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5A': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6A': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAA': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAA': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAA': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4B': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5B': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6B': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAB': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAB': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAB': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4C': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5C': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6C': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAC': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAC': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAC': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4D': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5D': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6D': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAD': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAD': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAD': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4E': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5E': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6E': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAE': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAE': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAE': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4F': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5F': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6F': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAF': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAF': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAF': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), '4H': Sizage(hs=2, ss=2, xs=0, fs=None, ls=0), '5H': Sizage(hs=2, ss=2, xs=0, fs=None, ls=1), '6H': Sizage(hs=2, ss=2, xs=0, fs=None, ls=2), '7AAH': Sizage(hs=4, ss=4, xs=0, fs=None, ls=0), '8AAH': Sizage(hs=4, ss=4, xs=0, fs=None, ls=1), '9AAH': Sizage(hs=4, ss=4, xs=0, fs=None, ls=2), } Codes = asdict(MtrDex) # map code name to code Names = {val : key for key, val in Codes.items()} # invert map code to code name Pad = '_' # B64 pad char for special codes with xtra size pre-padded soft values @classmethod def _rawSize(cls, code): """ Returns raw size in bytes not including leader for a given code Parameters: code (str): derivation code Base64 """ hs, ss, xs, fs, ls = cls.Sizes[code] # get sizes cs = hs + ss # both hard + soft code size if fs is None: raise InvalidCodeSizeError(f"Non-fixed raw size {code=}.") # assumes .Sizes only has valid entries, cs % 4 != 3, and fs % 4 == 0 return (((fs - cs) * 3 // 4) - ls) @classmethod def _fullSize(cls, code): """Fullsize of non-variable sized primitives indicated by code Returns: fullsize (int): full size in bytes for a given non-variable sized primitive as indicated by code When variable size raise InvalideCodeSizeError Parameters: code (str): derivation code Base64 """ _, _, _, fs, _ = cls.Sizes[code] # get sizes if fs is None: raise InvalidCodeSizeError(f"Non-fixed full size {code=}.") return fs @classmethod def _leadSize(cls, code): """ Returns lead size in bytes for a given code Parameters: code (str): derivation code Base64 """ _, _, _, _, ls = cls.Sizes[code] # get lead size from .Sizes table return ls @classmethod def _xtraSize(cls, code): """ Returns xtra size in bytes for a given code Parameters: code (str): derivation code Base64 """ _, _, xs, _, _ = cls.Sizes[code] # get lead size from .Sizes table return xs @classmethod def _special(cls, code): """ Returns: special (bool): True when code has special soft i.e. when fs is not None and ss > 0 False otherwise """ hs, ss, xs, fs, ls = cls.Sizes[code] return (fs is not None and ss > 0)
[docs] def __init__(self, raw=None, code=MtrDex.Ed25519N, soft='', rize=None, qb64b=None, qb64=None, qb2=None, strip=False, **kwa): """ Validate as fully qualified Parameters: raw (bytes | bytearray | None): unqualified crypto material usable for crypto operations. code (str): stable (hard) part of derivation code soft (str | bytes): soft part exclusive of prepad for special codes rize (int | None): raw size in bytes when variable sized material not including lead bytes if any Otherwise None qb64b (str | bytes | bytearray | memoryview | None): fully qualified crypto material Base64. When str, encodes as utf-8. Strips when bytearray and strip is True. qb64 (str | bytes | bytearray | memoryview | None): fully qualified crypto material Base64. When str, encodes as utf-8. Ignores strip qb2 (bytes | bytearray | memoryview | None): fully qualified crypto material Base2. Strips when bytearray and strip is True. strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip Needs either (raw and code and optionally rsize) or qb64b or qb64 or qb2 Otherwise raises EmptyMaterialError When raw and code and optional rsize provided then validate that code is correct for length of raw, rsize, computed size from Sizes and assign .raw Else when qb64b or qb64 or qb2 provided extract and assign .raw and .code and .size and .rsize """ if hasattr(soft, "decode"): # make soft str soft = soft.decode("utf-8") if raw is not None: # raw provided but may be empty if not code: raise EmptyMaterialError(f"Improper initialization need either " f"(raw not None and code) or " f"(code and soft) or " f"qb64b or qb64 or qb2.") if not isinstance(raw, (bytes, bytearray)): raise TypeError(f"Not a bytes or bytearray {raw=}.") if code not in self.Sizes: raise InvalidCodeError(f"Unsupported {code=}.") hs, ss, xs, fs, ls = self.Sizes[code] # assumes unit tests force valid sizes if fs is None: # variable sized assumes code[0] in SmallVrzDex or LargeVrzDex # assumes xs must be 0 when variable sized if rize: # use rsize to determine length of raw to extract if rize < 0: raise InvalidVarRawSizeError(f"Missing var raw size for " f"code={code}.") else: # use length of provided raw as rize rize = len(raw) ls = (3 - (rize % 3)) % 3 # calc actual lead (pad) size # raw binary size including leader in bytes size = (rize + ls) // 3 # calculate value of size in triplets if code[0] in SmallVrzDex: # compute code with sizes if size <= (64 ** 2 - 1): # ss = 2 hs = 2 s = astuple(SmallVrzDex)[ls] code = f"{s}{code[1:hs]}" ss = 2 elif size <= (64 ** 4 - 1): # ss = 4 make big version of code hs = 4 s = astuple(LargeVrzDex)[ls] code = f"{s}{'A' * (hs - 2)}{code[1]}" soft = intToB64(size, 4) ss = 4 else: raise InvalidVarRawSizeError(f"Unsupported raw size for " f"{code=}.") elif code[0] in LargeVrzDex: # compute code with sizes if size <= (64 ** 4 - 1): # ss = 4 hs = 4 s = astuple(LargeVrzDex)[ls] code = f"{s}{code[1:hs]}" ss = 4 else: raise InvalidVarRawSizeError(f"Unsupported raw size for large " f"{code=}. {size} <= {64 ** 4 - 1}") else: raise InvalidVarRawSizeError(f"Unsupported variable raw size " f"{code=}.") soft = intToB64(size, ss) else: # fixed size but raw may be empty and/or special soft rize = Matter._rawSize(code) # get raw size from Sizes for code # if raw then ls may be nonzero if ss > 0: # special soft size, so soft must be provided soft = soft[:ss-xs] # if len(soft) != ss - xs: raise SoftMaterialError(f"Not enough chars in {soft=} " f"with {ss=} {xs=} for {code=}.") if not Reb64.match(soft.encode("utf-8")): raise InvalidSoftError(f"Non Base64 chars in {soft=}.") else: soft = '' # must be empty when ss == 0 raw = raw[:rize] # copy only exact size from raw stream if len(raw) != rize: # forbids shorter raise RawMaterialError(f"Not enougth raw bytes for code={code}" f"expected {rize=} got {len(raw)}.") self._code = code # str hard part of full code self._soft = soft # str soft part of full code exclusive of xs prepad, empty when ss=0 self._raw = bytes(raw) # crypto ops require bytes not bytearray elif soft and code: # raw None so ls == 0 with fixed size and special hs, ss, xs, fs, ls = self.Sizes[code] # assumes unit tests force valid sizes if not fs: # variable sized code so can't be special soft raise InvalidSoftError(f"Unsupported variable sized {code=} " f" with {fs=} for special {soft=}.") if not ss > 0 or (fs == hs + ss and not ls == 0): # not special soft raise InvalidSoftError("Invalid soft size={ss} or lead={ls} " f" or {code=} {fs=} when special soft.") soft = soft[:ss-xs] if len(soft) != ss - xs: raise SoftMaterialError(f"Not enough chars in {soft=} " f"with {ss=} {xs=} for {code=}.") if not Reb64.match(soft.encode("utf-8")): raise InvalidSoftError(f"Non Base64 chars in {soft=}.") self._code = code # str hard part of code self._soft = soft # str soft part of code, empty when ss=0 self._raw = b'' # force raw empty when None given and special soft elif qb64b is not None: self._exfil(qb64b) if strip: # assumes bytearray del qb64b[:self.fullSize] elif qb64 is not None: self._exfil(qb64) elif qb2 is not None: self._bexfil(qb2) if strip: # assumes bytearray del qb2[:self.fullSize * 3 // 4] else: raise EmptyMaterialError(f"Improper initialization need either " f"(raw not None and code) or " f"(code and soft) or " f"qb64b or qb64 or qb2.")
@property def code(self): """ Returns: code (str): hard part only of full text code. Getter for ._code. Makes ._code read only Some codes only have a hard part. Soft part may be for variable sized matter or for special codes that are code only (raw is empty) """ return self._code @property def name(self): """ Returns: name (str): code name for self.code. Used for annotation for primitives like Matter """ return self.Names[self.code] @property def hard(self): """ Returns: hard (str): hard part only of full text code. Alias for .code. """ return self.code @property def soft(self): """ Returns: soft (str): soft part only of full text code. Getter for ._soft. Make ._soft read only """ return self._soft @property def size(self): """ Returns: size(int | None): Number of variably sized b64 quadlets/b2 triplets in primitive when varibly sized None when not variably sized when (fs!=None) Number of quadlets/triplets of chars/bytes of variable sized material or None when not variably sized. Converted qb64 value to int of soft ss portion of full text code when variably sized primitive material (fs == None). """ return (b64ToInt(self.soft) if self.soft else None) @property def both(self): """ Returns: both (str): hard + soft parts of full text code """ #_, ss, _, _ = self.Sizes[self.code] #if self.size is not None: #return (f"{self.code}{intToB64(self.size, l=ss)}") #else: #return (f"{self.code}{self.soft}") _, _, xs, _, _ = self.Sizes[self.code] return (f"{self.code}{self.Pad * xs}{self.soft}") @property def fullSize(self): """Full size of primitive for both fixed and variable size primitives Returns: fullsize (int): full size of primitive in bytes Fixed size codes returns fs from .Sizes Variable size codes where fs==None computes fs from .size and sizes """ hs, ss, _, fs, _ = self.Sizes[self.code] # get sizes if fs is None: # compute fs from ss characters in code fs = hs + ss + (self.size * 4) return fs @property def raw(self): """ Returns ._raw Makes .raw read only """ return self._raw @property def qb64b(self): """ Property qb64b: Returns Fully Qualified Base64 Version encoded as bytes Assumes self.raw and self.code are correctly populated """ return self._infil() @property def qb64(self): """ Property qb64: Returns Fully Qualified Base64 Version Assumes self.raw and self.code are correctly populated """ return self.qb64b.decode("utf-8") @property def qb2(self): """ Property qb2: Returns Fully Qualified Binary Version Bytes """ return self._binfil() @property def transferable(self): """ Property transferable: Returns True if identifier does not have non-transferable derivation code, False otherwise """ return (self.code not in NonTransDex) @property def digestive(self): """ Property digestable: Returns True if identifier has digest derivation code, False otherwise """ return (self.code in DigDex) @property def prefixive(self): """ Property prefixive: Returns True if identifier has prefix derivation code, False otherwise """ return (self.code in PreDex) @property def special(self): """ special (bool): True when self.code has special self.soft i.e. when fs is not None and ss > 0 and fs = hs + ss and ls = 0 i.e. (fs fixed and soft not empty and raw is empty and no lead) False otherwise """ return self._special(self.code) @property def composable(self): """ composable (bool): True when both .qb64b and .qb2 are 24 bit aligned and round trip using encodeB64 and decodeB64. False otherwise """ qb64b = self.qb64b qb2 = self.qb2 return (len(qb64b) % 4 == 0 and len(qb2) % 3 == 0 and encodeB64(qb2) == qb64b and decodeB64(qb64b) == qb2) def _infil(self): """Create b64 text domain representation from raw and code Returns: primitive (bytes): fully qualified base64 characters. """ code = self.code # hard part of full code == codex value both = self.both # code + soft, soft may be empty raw = self.raw # bytes or bytearray, raw may be empty rs = len(raw) # raw size hs, ss, xs, fs, ls = self.Sizes[code] cs = hs + ss # assumes unit tests on Matter.Sizes ensure valid size entries if cs != len(both): InvalidCodeSizeError(f"Invalid full code={both} for sizes {hs=} and" f" {ss=}.") if not fs: # variable sized # Tests on .Sizes table must ensure ls in (0,1,2) and cs % 4 == 0 but # can't know the variable size. So instance methods must ensure that # (ls + rs) % 3 == 0 i.e. both full code (B64) and lead+raw (B2) # are both 24 bit aligned. # If so then should not need following check. if (ls + rs) % 3 or cs % 4: raise InvalidCodeSizeError(f"Invalid full code{both=} with " f"variable raw size={rs} given " f" {cs=}, {hs=}, {ss=}, {fs=}, and " f"{ls=}.") # When ls+rs is 24 bit aligned then encodeB64 has no trailing # pad chars that need to be stripped. So simply prepad raw with # ls zero bytes and convert (encodeB64). full = (both.encode("utf-8") + encodeB64(bytes([0] * ls) + raw)) else: # fixed size ps = (3 - ((rs + ls) % 3)) % 3 # net pad size given raw with lead # net pad size must equal both code size remainder so that primitive # both + converted padded raw is fs long. Assumes ls in (0,1,2) and # cs % 4 != 3, fs % 4 == 0. Sizes table test must ensure these properties. # If so then should not need following check. if ps != (cs % 4): # given cs % 4 != 3 then cs % 4 is pad size raise InvalidCodeSizeError(f"Invalid full code{both=} with " f"fixed raw size={rs} given " f" {cs=}, {hs=}, {ss=}, {fs=}, and " f"{ls=}.") # Predpad raw so we midpad the full primitive. Prepad with ps+ls # zero bytes ensures encodeB64 of prepad+lead+raw has no trailing # pad characters. Finally skip first ps == cs % 4 of the converted # characters to ensure that when full code is prepended, the full # primitive size is fs but midpad bits are zeros. full = (both.encode("utf-8") + encodeB64(bytes([0] * (ps + ls)) + raw)[ps:]) if (len(full) % 4) or (fs and len(full) != fs): raise InvalidCodeSizeError(f"Invalid full size given code{both=} " f" with raw size={rs}, {cs=}, {hs=}, " f"{ss=}, {xs=} {fs=}, and {ls=}.") return full def _binfil(self): """Create qb2 binary domain representation from raw and code Returns bytes of fully qualified base2 bytes, that is .qb2 self.code converted to Base2 + self.raw left shifted with pad bits equivalent of Base64 decode of .qb64 into .qb2 """ code = self.code # hard part of full code == codex value both = self.both # code + soft, soft may be empty raw = self.raw # bytes or bytearray may be empty hs, ss, xs, fs, ls = self.Sizes[code] cs = hs + ss # assumes unit tests on Matter.Sizes ensure valid size entries n = sceil(cs * 3 / 4) # number of b2 bytes to hold b64 code # convert code both to right align b2 int then left shift in pad bits # then convert to bytes bcode = (b64ToInt(both) << (2 * (cs % 4))).to_bytes(n, 'big') full = bcode + bytes([0] * ls) + raw # includes lead bytes bfs = len(full) if not fs: # compute fs fs = hs + ss + (len(raw) + ls) * 4 // 3 # hs + ss + (size * 4) if bfs % 3 or (bfs * 4 // 3) != fs: # invalid size raise InvalidCodeSizeError(f"Invalid full code={both} for raw size" f"={len(raw)}.") return full def _exfil(self, qb64b): """Extracts self.code and self.raw from qualified base64 qb64b of type str or bytes or bytearray or memoryview Detects if str and converts to bytes Parameters: qb64b (str|bytes|bytearray|memoryview): fully qualified base64 from stream """ if not qb64b: # empty need more bytes raise ShortageError("Empty material.") first = qb64b[:1] # extract first char code selector if isinstance(first, memoryview): first = bytes(first) if hasattr(first, "decode"): first = first.decode() # converts bytes/bytearray to str if first not in self.Hards: if first[0] == '-': raise UnexpectedCountCodeError("Unexpected count code start" "while extracting Matter.") elif first[0] == '_': raise UnexpectedOpCodeError("Unexpected op code start" "while extracing Matter.") else: raise UnexpectedCodeError(f"Unsupported code start char={first}.") hs = self.Hards[first] # get hard code size if len(qb64b) < hs: # need more bytes raise ShortageError(f"Need {hs - len(qb64b)} more characters.") hard = qb64b[:hs] # extract hard code if isinstance(hard, memoryview): hard = bytes(hard) if hasattr(hard, "decode"): hard = hard.decode() # converts bytes/bytearray to str if hard not in self.Sizes: raise UnexpectedCodeError(f"Unsupported code ={hard}.") hs, ss, xs, fs, ls = self.Sizes[hard] # assumes hs in both tables match cs = hs + ss # both hs and ss # assumes that unit tests on Matter .Sizes .Hards and .Bards ensure that # these are well formed. # when fs is None then ss > 0 otherwise fs > hs + ss when ss > 0 # extract soft chars including xtra, empty when ss==0 and xs == 0 # assumes that when ss == 0 then xs must be 0 soft = qb64b[hs:hs+ss] if isinstance(soft, memoryview): soft = bytes(soft) if hasattr(soft, "decode"): soft = soft.decode() # converts bytes/bytearray to str xtra = soft[:xs] # extract xtra if any from front of soft soft = soft[xs:] # strip xtra from soft if xtra != f"{self.Pad * xs}": raise UnexpectedCodeError(f"Invalid prepad xtra ={xtra}.") if not fs: # compute fs from soft from ss part which provides size B64 # compute variable size as int may have value 0 fs = (b64ToInt(soft) * 4) + cs if len(qb64b) < fs: # need more bytes raise ShortageError(f"Need {fs - len(qb64b)} more chars.") qb64b = qb64b[:fs] # fully qualified primitive code plus material if isinstance(qb64b, memoryview): qb64b = bytes(qb64b) if hasattr(qb64b, "encode"): # only convert extracted chars from stream qb64b = qb64b.encode() # converts str to bytes # check for non-zeroed pad bits and/or lead bytes # net prepad ps == cs % 4 (remainer). Assumes ps != 3 i.e ps in (0,1,2) # To ensure number of prepad bytes and prepad chars are same. # need net prepad chars ps to invert using decodeB64 of lead + raw ps = cs % 4 # net prepad bytes to ensure 24 bit align when encodeB64 base = ps * b'A' + qb64b[cs:] # prepad ps 'A's to B64 of (lead + raw) paw = decodeB64(base) # now should have ps + ls leading sextexts of zeros raw = paw[ps+ls:] # remove prepad midpat bytes to invert back to raw # ensure midpad bytes are zero pi = int.from_bytes(paw[:ps+ls], "big") if pi != 0: raise ConversionError(f"Nonzero midpad bytes=0x{pi:0{(ps + ls) * 2}x}.") if len(raw) != ((len(qb64b) - cs) * 3 // 4) - ls: # exact lengths raise ConversionError(f"Improperly qualified material = {qb64b}") self._code = hard # hard only str self._soft = soft # soft only str self._raw = raw # ensure bytes for crypto ops, may be empty def _bexfil(self, qb2): """Extracts self.code and self.raw from qualified base2 qb2 Parameters: qb2 (bytes | bytearray | memoryview): fully qualified base2 from stream """ if not qb2: # empty need more bytes raise ShortageError("Empty material, Need more bytes.") first = nabSextets(qb2, 1) # extract first sextet as code selector if first not in self.Bards: if first[0] == b'\xf8': # b64ToB2('-') raise UnexpectedCountCodeError("Unexpected count code start" "while extracing Matter.") elif first[0] == b'\xfc': # b64ToB2('_') raise UnexpectedOpCodeError("Unexpected op code start" "while extracing Matter.") else: raise UnexpectedCodeError(f"Unsupported code start sextet={first}.") hs = self.Bards[first] # get code hard size equvalent sextets bhs = sceil(hs * 3 / 4) # bhs is min bytes to hold hs sextets if len(qb2) < bhs: # need more bytes raise ShortageError(f"Need {bhs - len(qb2)} more bytes.") hard = codeB2ToB64(qb2, hs) # extract and convert hard part of code if hard not in self.Sizes: raise UnexpectedCodeError(f"Unsupported code ={hard}.") hs, ss, xs, fs, ls = self.Sizes[hard] cs = hs + ss # both hs and ss # assumes that unit tests on Matter .Sizes .Hards and .Bards ensure that # these are well formed. # when fs is None then ss > 0 otherwise fs > hs + ss when ss > 0 bcs = sceil(cs * 3 / 4) # bcs is min bytes to hold cs sextets if len(qb2) < bcs: # need more bytes raise ShortageError("Need {} more bytes.".format(bcs - len(qb2))) both = codeB2ToB64(qb2, cs) # extract and convert both hard and soft part of code # extract soft chars including xtra, empty when ss==0 and xs == 0 # assumes that when ss == 0 then xs must be 0 soft = both[hs:hs+ss] # get soft may be empty xtra = soft[:xs] # extract xtra if any from front of soft soft = soft[xs:] # strip xtra from soft if xtra != f"{self.Pad * xs}": raise UnexpectedCodeError(f"Invalid prepad xtra ={xtra}.") if not fs: # compute fs from size chars in ss part of code if len(qb2) < bcs: # need more bytes raise ShortageError("Need {} more bytes.".format(bcs - len(qb2))) # compute size as int from soft part given by ss B64 chars fs = (b64ToInt(soft) * 4) + cs # compute fs bfs = sceil(fs * 3 / 4) # bfs is min bytes to hold fs sextets if len(qb2) < bfs: # need more bytes raise ShortageError("Need {} more bytes.".format(bfs - len(qb2))) qb2 = qb2[:bfs] # extract qb2 fully qualified primitive code plus material # check for nonzero trailing full code mid pad bits ps = cs % 4 # full code (both) net pad size for 24 bit alignment pbs = 2 * ps # mid pad bits = 2 per net pad # get pad bits in last byte of full code pi = (int.from_bytes(qb2[bcs-1:bcs], "big")) # convert byte to int pi = pi & (2 ** pbs - 1 ) # mask with 1's in pad bit locations if pi: # not zero so raise error raise ConversionError(f"Nonzero code mid pad bits=0b{pi:0{pbs}b}.") # check nonzero leading mid pad lead bytes in lead + raw li = int.from_bytes(qb2[bcs:bcs+ls], "big") # lead as int if li: # midpad lead bytes must be zero raise ConversionError(f"Nonzero lead midpad bytes=0x{li:0{ls*2}x}.") # strip code and leader bytes from qb2 to get raw raw = qb2[(bcs + ls):] # may be empty if len(raw) != (len(qb2) - bcs - ls): # exact lengths raise ConversionError(r"Improperly qualified material = {qb2}") self._code = hard # hard only self._soft = soft # soft only may be empty self._raw = bytes(raw) # ensure bytes for crypto ops may be empty
[docs] class Seqner(Matter): """ Seqner is subclass of Matter, cryptographic material, for fully qualified fixed serialization sized ordinal numbers such as sequence numbers or first seen numbers. The serialization is forced to a fixed size (single code) so that it may be used for lexocographically ordered namespaces such as database indices. That code is MtrDex.Salt_128 Default initialization code = MtrDex.Salt_128 Raises error on init if code is not MtrDex.Salt_128 Attributes: Inherited Properties: (See Matter) .pad is int number of pad chars given raw .code is str derivation code to indicate cypher suite .raw is bytes crypto material only without code .index is int count of attached crypto material by context (receipts) .qb64 is str in Base64 fully qualified with derivation code + crypto mat .qb64b is bytes in Base64 fully qualified with derivation code + crypto mat .qb2 is bytes in binary with derivation code + crypto material .transferable is Boolean, True when transferable derivation code False otherwise Properties: .sn is int sequence number .snh is hex string representation of sequence number no leading zeros Hidden: ._pad is method to compute .pad property ._code is str value for .code property ._raw is bytes value for .raw property ._index is int value for .index property ._infil is method to compute fully qualified Base64 from .raw and .code ._exfil is method to extract .code and .raw from fully qualified Base64 Methods: """
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.Salt_128, sn=None, snh=None, **kwa): """ Inherited Parameters: (see Matter) raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes Parameters: sn (int | str | None): some form of ordinal number int or hex str snh (str | None): hex string of ordinal number """ if raw is None and qb64b is None and qb64 is None and qb2 is None: try: if sn is None: if snh is None or snh == '': sn = 0 else: sn = int(snh, 16) else: # sn is not None but so may be hex str if isinstance(sn, str): # is it a hex str if sn == '': sn = 0 else: sn = int(sn, 16) except ValueError as ex: raise InvalidValueError(f"Not whole number={sn} .") from ex if not isinstance(sn, int) or sn < 0: raise InvalidValueError(f"Not whole number={sn}.") if sn > MaxON: # too big for ordinal 256 ** 16 - 1 raise ValidationError(f"Non-ordinal {sn} exceeds {MaxON}.") raw = sn.to_bytes(Matter._rawSize(MtrDex.Salt_128), 'big') super(Seqner, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code != MtrDex.Salt_128: raise ValidationError("Invalid code = {} for Seqner." "".format(self.code))
@property def sn(self): """ Property sn: sequence number as int Returns .raw converted to int """ return int.from_bytes(self.raw, 'big') @property def snh(self): """ Property snh: sequence number as hex Returns .sn int converted to hex str """ return f"{self.sn:x}" # "{:x}".format(self.sn)
[docs] class Number(Matter): """ Number is subclass of Matter, cryptographic material, for ordinal counting whole numbers (non-negative integers) up to a maximum size of 16 bytes, 256 ** 16 - 1. Examples uses are sequence numbers or first seen ordering numbers or thresholds. Seqner provides fully qualified format for ordinals (sequence numbers etc) when provided as attached cryptographic material elements. Useful when parsing attached receipt groupings with sn from stream or database Uses default initialization code = CryTwoDex.Salt_128 Raises error on init if code not CryTwoDex.Salt_128 Attributes: Inherited Properties: (See Matter) code (str): hard part of derivation code to indicate cypher suite both (int): hard and soft parts of full text code size (int): Number of triplets of bytes including lead bytes (quadlets of chars) of variable sized material. Value of soft size, ss, part of full text code. Otherwise None. rize (int): number of bytes of raw material not including lead bytes raw (bytes): crypto material only without code qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise Properties: num (int): int representation of number humh (str): hex string representation of number with no leading zeros sn (int): alias for num snh (str): alias for numh huge (str): qb64 of num but with code NumDex.Huge so 24 char compatible with fixed size seq num for lexicographic lmdb key space positive (bool): True if .num > 0, False otherwise. Because .num must be non-negative, .positive == False means .num == 0 inceptive (bool): True means .num == 0 False otherwise. Hidden: _code (str): value for .code property _raw (bytes): value for .raw property _rsize (bytes): value for .rsize property. Raw size in bytes when variable sized material else None. _size (int): value for .size property. Number of triplets of bytes including lead bytes (quadlets of chars) of variable sized material else None. _infil (types.MethodType): creates qb64b from .raw and .code (fully qualified Base64) _exfil (types.MethodType): extracts .code and .raw from qb64b (fully qualified Base64) Methods: """ Codes = asdict(NumDex) # map code name to code Names = {val : key for key, val in Codes.items()} # invert map code to code name
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=None, num=None, numh=None, sn=None, snh=None, **kwa): """ Inherited Parameters: (see Matter) raw (bytes): unqualified crypto material usable for crypto operations code (str | None): stable (hard) part of derivation code. None means pick code based on value of num or numh otherwise raise error rize (int): raw size in bytes when variable sized material else None qb64b (bytes): fully qualified crypto material Base64 qb64 (str, bytes): fully qualified crypto material Base64 qb2 (bytes): fully qualified crypto material Base2 strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip Parameters: num (int | str | None): non-negative int number or hex str of int number or 0 if None numh (str): hex string equivalent of non-negative int number sn (int | str | None): alias of num for backwards compat with Seqner snh (str): alias of numh for backwards compat with Seqner Note: int("0xab", 16) is also valid since int recognizes 0x hex prefix """ if raw is None and qb64b is None and qb64 is None and qb2 is None: try: num = num if num is not None else sn numh = numh if numh is not None else snh if num is None: if numh is None or numh == '': num = 0 elif isinstance(numh, int): num = numh else: num = int(numh, 16) else: # handle case where num is hex str' if isinstance(num, str): if num == '': num = 0 else: num = int(num, 16) if num < 0: raise InvalidValueError(f"Not whole number={num}") except ValueError as ex: raise InvalidValueError(f"Not whole number={num}") from ex if code is None: # dynamically size code if not isinstance(num, int) or num < 0: raise InvalidValueError(f"Not whole number={num}.") if num <= (256 ** 2 - 1): # make short version of code code = NumDex.Short elif num <= (256 ** 5 - 1): # make tall version of code code = code = NumDex.Tall elif num <= (256 ** 8 - 1): # make big version of code code = code = NumDex.Big elif num <= (256 ** 11 - 1): # make large version of code code = code = NumDex.Large elif num <= (256 ** 14 - 1): # make great version of code code = code = NumDex.Great elif num <= (256 ** 17 - 1): # make vast version of code code = code = NumDex.Vast else: raise InvalidValueError(f"Invalid num = {num}, too large to encode.") # default to_bytes parameter signed is False. If negative raises # OverflowError: can't convert negative int to unsigned try: raw = num.to_bytes(Matter._rawSize(code), 'big') # big endian unsigned except Exception as ex: raise InvalidValueError(f"Not convertable to bytes {num=}.") from ex if len(raw) > Matter._rawSize(code): raise InvalidValueError(f"To big {num=} for {code=}.") super(Number, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code not in NumDex: raise ValidationError(f"Invalid code = {self.code} for Number.")
[docs] def validate(self, inceptive=None): """ Returns: self (Number): Raises: ValidationError: when .num is invalid ordinal such as sequence number or first seen number etc. Parameters: inceptive(bool): raise ValidationError whan .num invalid None means exception when .num < 0 True means exception when .num != 0 False means exception when .num < 1 """ num = self.num if num > MaxON: # too big for ordinal 256 ** 16 - 1 raise ValidationError(f"Non-ordinal {num} exceeds {MaxON}.") if inceptive is not None: if inceptive: if num != 0: raise ValidationError(f"Nonzero num = {num} non-inceptive" f" ordinal.") else: if num < 1: raise ValidationError(f"Non-positive num = {num} not " f"non-inceptive ordinal.") else: if num < 0: raise ValidationError(f"Negative num = {num} non-ordinal.") return self
@property def num(self): """ Property num: number as int Returns .raw converted to int """ return int.from_bytes(self.raw, 'big') @property def numh(self): """ Property numh: number as hex string no leading zeros Returns .num int converted to hex str """ return f"{self.num:x}" @property def sn(self): """Sequence number, sn property getter to mimic Seqner interface Returns: sn (int): alias for num """ return self.num @property def snh(self): """Sequence number hex str, snh property getter to mimic Seqner interface Returns: snh (hex str): alias for numh """ return self.numh @property def huge(self): """Provides number value as qb64 but with code NumDex.huge. This is the same as Seqner.qb64. Raises error if too big. Returns: huge (str): qb64 of num coded as NumDex.Huge """ num = self.num if num > MaxON: # too big for ordinal 256 ** 16 - 1 raise InvalidValueError(f"Non-ordinal {num} exceeds {MaxON}.") return Number(num=num, code=NumDex.Huge).qb64 @property def positive(self): """ Returns True if .num is strictly positive non-zero False otherwise. Because valid number .num must be non-negative, positive False also means that .num is zero. """ return True if self.num > 0 else False @property def inceptive(self): """ Returns True if .num == 0 False otherwise. """ return True if self.num == 0 else False
[docs] class Decimer(Matter): """Decimer is subclass of Matter, for variable length decimal number strings (dns) that are translated to Base64 and converted in Base2 raw for compactness. Decimer encoded decimal number strings only contain Base64 URL safe characters that map to decimal number strings that can be converted to decimal numbers. These strings support signed and unsigned, int and float. The numeric value can be retrieved with the .decimal property. When created using the dns parameter, the encoded matter in qb64 format in the text domain is more compact than would be the case if the string were Base64 encoded from a utf-8 decimal string. Example: Decimer(dns='-5.6').qb64 == 'BBB' Decimer(dns='-5.6').raw == 'BBB' Decimer(dns='-5.6').dns == '-5.6' Decimer(dns='-5.6').decimal == -5.6 Decimer(decimal=-5.6).dns = '-5.6' Attributes: (See Matter) Inherited Properties: (See Matter) Properties: .dns (str): decimal number string .decimal (int|float): decimal number Inherited Hidden Properties: (See Matter) Methods: ._rawify(self, dns) Codes: (See DecimalCodex or DecDex) Decimal_L0: str = '4H' # Decimal B64 string float and int lead size 0 Decimal_L1: str = '5H' # Decimal B64string float and int lead size 1 Decimal_L2: str = '6H' # Decimal B64string float and intlead size 2 Decimal_Big_L0: str = '7AAH' # Decimal B64string float and int big lead size 0 Decimal_Big_L1: str = '8AAH' # Decimal B64string float and int big lead size 1 Decimal_Big_L2: str = '9AAH' # Decimal B64string float and int big lead size 2 """ ToB64 = str.maketrans(".", "p") # translate characters FromB64 = str.maketrans("p", ".") # translate characters
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.Decimal_L0, dns=None, decimal=None, **kwa): """ Inherited Parameters: (see Matter) raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes Parameters: dns (str|bytes): decimal number string decimal (int|float): decimal number """ if raw is None and qb64b is None and qb64 is None and qb2 is None: if decimal is not None: dns = f"{decimal}" if dns is None: raise EmptyMaterialError(f"Need one of raw, qb64, qb64b, qb2, " f"dns, or decimer input") if hasattr(dns, "decode"): dns = dns.decode("") # convert to str # make round trippable extra leading 0's , + try: decimal = int(dns) # make round trippable -0 +0 leading 0's dns = f"{decimal}" # removes leading + - extra 0's except ValueError: # float not int decimal = float(dns) # make round trippable dns = f"{decimal}" # -0.0 is valid, float('-0.0') is -0.0 raw = self._rawify(dns) super(Decimer, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code not in DecDex: raise ValidationError(f"Invalid code={self.code} for Decimer")
def _rawify(self, dns): """Returns raw value equivalent of dns. Suitable for variable sized matter Parameters: dns (str): decimal number string """ dns = dns.translate(self.ToB64).encode() # replace period with 'p' ts = len(dns) % 4 # dns size mod 4 ws = (4 - ts) % 4 # pre conv wad size in chars ls = (3 - ts) % 3 # post conv lead size in bytes base = b'A' * ws + dns # pre pad with wad of zeros in Base64 == 'A' raw = decodeB64(base)[ls:] # convert and remove leader return raw # raw binary equivalent of dns @property def dns(self): """Returns decoded raw as dns Returns: dns (str): decoded raw decimal number string """ _, _, _, _, ls = self.Sizes[self.code] dns = encodeB64(bytes([0] * ls) + self.raw) ws = 0 if ls == 0 and dns: if dns[0] == ord(b'A'): # strip leading 'A' zero pad ws = 1 else: ws = (ls + 1) % 4 dns = dns[ws:].decode() # strip padding return dns.translate(self.FromB64) # replace 'p' with period if any @property def decimal(self): """Property decimal: raw as int or float Returns: decimal (int|float): .raw converted to int or float as appropriate """ dns = self.dns try: return (int(dns)) except ValueError: return float(dns)
[docs] class Dater(Matter): """ Dater is subclass of Matter, cryptographic material, for RFC-3339 profile of ISO-8601 formatted datetimes. Dater provides a custom Base64 coding of an ASCII RFC-3339 profile of an ISO-8601 datetime by replacing (using translate) the three non-Base64 characters, ':.+' with the Base64 equivalents, 'cdp' respectively. Dater provides a more compact representation than would be obtained by converting the raw ASCII RFC-3339 profile ISO-8601 datetime to Base64. Dater supports datetimes as attached crypto material in replay of events for the datetime of when the event was first seen. The datetime textual representation is restricted to a specific 32 byte variant (profile) of ISO-8601 datetime with microseconds and UTC offset in HH:MM (See RFC-3339). Uses default initialization derivation code = MtrDex.DateTime. Raises error on init if code not MtrDex.DateTime Examples: given RFC-3339 profiles of ISO-8601 datetime strings: '2020-08-22T17:50:09.988921+00:00' '2020-08-22T17:50:09.988921-01:00' The fully encoded qualified Base64, .qb64 versions are respectively '1AAG2020-08-22T17c50c09d988921p00c00' '1AAG2020-08-22T17c50c09d988921-01c00' The qualified binary version, .qb2 is the Base64 decoding the qualified Base64, qb64, '1AAG2020-08-22T17c50c09d988921p00c00' The raw binary of the fully encoded version is the Base64 decoding of the the datetime only portion, '2020-08-22T17c50c09d988921p00c00' Use the properties to get the different representations .dts is ASCII RFC-3339 of ISO-8601 .qb64 is qualified Base64 encoding with derivation code proem and ':.+' replaced with 'cdp' .qb2 is qualified binary decoding of the .qb64 .code is text CESR derivation code .raw is binary version of the converted datetime only portion of .qb64 Example uses: attached first seen couples with fn+dt Attributes: Inherited Properties: (See Matter) .pad is int number of pad chars given raw .code is str derivation code to indicate cypher suite .raw is bytes crypto material only without code .index is int count of attached crypto material by context (receipts) .qb64 is str in Base64 fully qualified with derivation code + crypto mat .qb64b is bytes in Base64 fully qualified with derivation code + crypto mat .qb2 is bytes in binary with derivation code + crypto material .transferable is Boolean, True when transferable derivation code False otherwise Properties: .dts is the ISO-8601 datetime string Hidden: ._pad is method to compute .pad property ._code is str value for .code property ._raw is bytes value for .raw property ._index is int value for .index property ._infil is method to compute fully qualified Base64 from .raw and .code ._exfil is method to extract .code and .raw from fully qualified Base64 Methods: """ ToB64 = str.maketrans(":.+", "cdp") # translate characters FromB64 = str.maketrans("cdp", ":.+") # translate characters
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.DateTime, dts=None, **kwa): """ Inherited Parameters: (see Matter) raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes Parameters: dts is the ISO-8601 datetime as str or bytes """ if raw is None and qb64b is None and qb64 is None and qb2 is None: if dts is None: # defaults to now dts = nowIso8601() if hasattr(dts, "decode"): dts = dts.decode("utf-8") qb64 = MtrDex.DateTime + dts.translate(self.ToB64) super(Dater, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code != MtrDex.DateTime: raise ValidationError("Invalid code = {} for Dater date time." "".format(self.code))
@property def dts(self): """ Property dts: date-time-stamp str Returns .qb64 translated to RFC-3339 profile of ISO 8601 datetime str """ return self.qb64[self.Sizes[self.code].hs:].translate(self.FromB64) @property def dtsb(self): """ Property dtsb: date-time-stamp bytes Returns .qb64 translated to RFC-3339 profile of ISO 8601 datetime bytes """ return self.qb64[self.Sizes[self.code].hs:].translate(self.FromB64).encode("utf-8") @property def datetime(self): """ Property datetime: Returns datetime.datetime instance converted from .dts """ return fromIso8601(self.dts)
[docs] class Tagger(Matter): """ Tagger is subclass of Matter, cryptographic material, for compact special fixed size primitive with non-empty soft part and empty raw part. Tagger provides a more compact representation of small Base64 values in as soft part of code rather than would be obtained by by using a small raw part whose ASCII representation is converted to Base64. Attributes: Inherited Properties: (See Matter) code (str): hard part of derivation code to indicate cypher suite hard (str): hard part of derivation code. alias for code soft (str): soft part of derivation code fs any. Empty when ss = 0. both (str): hard + soft parts of full text code size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) fullSize (int): full size of primitive raw (bytes): crypto material only. Not derivation code or lead bytes. qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise prefixive (bool): True means identifier prefix derivation code False otherwise special (bool): True when soft is special raw is empty and fixed size composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip Properties: tag (str): B64 .soft portion of code but without prepad Inherited Hidden: (See Matter) _code (str): value for .code property _soft (str): soft value of full code _raw (bytes): value for .raw property _rawSize(): _leadSize(): _special(): _infil(): creates qb64b from .raw and .code (fully qualified Base64) _binfil(): creates qb2 from .raw and .code (fully qualified Base2) _exfil(): extracts .code and .raw from qb64b (fully qualified Base64) _bexfil(): extracts .code and .raw from qb2 (fully qualified Base2) Hidden: Methods: """
[docs] def __init__(self, tag='', soft='', code=None, **kwa): """ Inherited Parameters: (see Matter) raw (bytes | bytearray | None): unqualified crypto material usable for crypto operations. code (str): stable (hard) part of derivation code soft (str | bytes): soft part for special codes rize (int | None): raw size in bytes when variable sized material not including lead bytes if any Otherwise None qb64b (bytes | None): fully qualified crypto material Base64 qb64 (str | bytes | None): fully qualified crypto material Base64 qb2 (bytes | None): fully qualified crypto material Base2 strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip Parameters: tag (str | bytes): Base64 automatic sets code given size of tag """ if tag: if hasattr(tag, "encode"): # make tag bytes for regex tag = tag.encode("utf-8") if not Reb64.match(tag): raise InvalidSoftError(f"Non Base64 chars in {tag=}.") code = self._codify(tag=tag) soft = tag super(Tagger, self).__init__(soft=soft, code=code, **kwa) if (not self._special(self.code)) or self.code not in TagDex: raise InvalidCodeError(f"Invalid code={self.code} for Tagger.")
@staticmethod def _codify(tag): """Returns code for tag when tag is appropriately sized Base64 Parameters: tag (str | bytes): Base64 value Returns: code (str): derivation code for tag """ # TagDex tags appear in order of size 1 to 10, at indices 0 to 9 codes = astuple(TagDex) l = len(tag) if l < 1 or l > len(codes): raise InvalidSoftError(f"Invalid {tag=} size {l=}, empty or oversized.") return codes[l-1] # return code at index = len - 1 @property def tag(self): """Returns: tag (str): B64 primitive without prepad (alias of self.soft) """ return self.soft
[docs] class Ilker(Tagger): """ Ilker is subclass of Tagger, cryptographic material, for formatted message types (ilks) in Base64. Leverages Tagger support compact special fixed size primitives with non-empty soft part and empty raw part. Ilker provides a more compact representation than would be obtained by converting the raw ASCII representation to Base64. Attributes: Inherited Properties: (See Tagger) code (str): hard part of derivation code to indicate cypher suite hard (str): hard part of derivation code. alias for code soft (str): soft part of derivation code fs any. Empty when ss = 0. both (str): hard + soft parts of full text code size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) fullSize (int): full size of primitive raw (bytes): crypto material only. Not derivation code or lead bytes. qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise prefixive (bool): True means identifier prefix derivation code False otherwise special (bool): True when soft is special raw is empty and fixed size composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip tag (str): B64 primitive without prepad (strips prepad from soft) Properties: ilk (str): message type from Ilks of Ilkage Inherited Hidden: (See Tagger) _code (str): value for .code property _soft (str): soft value of full code _raw (bytes): value for .raw property _rawSize(): _leadSize(): _special(): _infil(): creates qb64b from .raw and .code (fully qualified Base64) _binfil(): creates qb2 from .raw and .code (fully qualified Base2) _exfil(): extracts .code and .raw from qb64b (fully qualified Base64) _bexfil(): extracts .code and .raw from qb2 (fully qualified Base2) Hidden: Methods: """
[docs] def __init__(self, qb64b=None, qb64=None, qb2=None, tag='', ilk='', **kwa): """ Inherited Parameters: (see Tagger) raw (bytes | bytearray | None): unqualified crypto material usable for crypto operations. code (str): stable (hard) part of derivation code soft (str | bytes): soft part for special codes rize (int | None): raw size in bytes when variable sized material not including lead bytes if any Otherwise None qb64b (bytes | None): fully qualified crypto material Base64 qb64 (str | bytes | None): fully qualified crypto material Base64 qb2 (bytes | None): fully qualified crypto material Base2 strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip tag (str | bytes): Base64 plain. Prepad is added as needed. Parameters: ilk (str): message type from Ilks of Ilkage """ if not (qb64b or qb64 or qb2): if ilk: tag = ilk super(Ilker, self).__init__(qb64b=qb64b, qb64=qb64, qb2=qb2, tag=tag, **kwa) if self.code not in (MtrDex.Tag3, ): raise InvalidCodeError(f"Invalid code={self.code} for Ilker " f"{self.ilk=}.") if self.ilk not in Ilks: raise InvalidSoftError(f"Ivalid ilk={self.ilk} for Ilker.")
@property def ilk(self): """Returns: tag (str): B64 primitive without prepad (strips prepad from soft) Alias for self.tag """ return self.tag
[docs] class Traitor(Tagger): """ Traitor is subclass of Tagger, cryptographic material, for formatted configuration traits for key events in Base64. Leverages Tagger support of compact special fixed size primitives with non-empty soft part and empty raw part. Traitor provides a more compact representation than would be obtained by converting the raw ASCII representation to Base64. Attributes: Inherited Properties: (See Tagger) code (str): hard part of derivation code to indicate cypher suite hard (str): hard part of derivation code. alias for code soft (str): soft part of derivation code fs any. Empty when ss = 0. both (str): hard + soft parts of full text code size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) fullSize (int): full size of primitive raw (bytes): crypto material only. Not derivation code or lead bytes. qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise prefixive (bool): True means identifier prefix derivation code False otherwise special (bool): True when soft is special raw is empty and fixed size composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip tag (str): B64 primitive without prepad (strips prepad from soft) Properties: trait (str): configuration trait B64 from TraitDex Inherited Hidden: (See Tagger) _code (str): value for .code property _soft (str): soft value of full code _raw (bytes): value for .raw property _rawSize(): _leadSize(): _special(): _infil(): creates qb64b from .raw and .code (fully qualified Base64) _binfil(): creates qb2 from .raw and .code (fully qualified Base2) _exfil(): extracts .code and .raw from qb64b (fully qualified Base64) _bexfil(): extracts .code and .raw from qb2 (fully qualified Base2) Hidden: Methods: """
[docs] def __init__(self, qb64b=None, qb64=None, qb2=None, tag='', trait='', **kwa): """ Inherited Parameters: (see Tagger) raw (bytes | bytearray | None): unqualified crypto material usable for crypto operations. code (str): stable (hard) part of derivation code soft (str | bytes): soft part for special codes rize (int | None): raw size in bytes when variable sized material not including lead bytes if any Otherwise None qb64b (bytes | None): fully qualified crypto material Base64 qb64 (str | bytes | None): fully qualified crypto material Base64 qb2 (bytes | None): fully qualified crypto material Base2 strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip tag (str | bytes): Base64 plain. Prepad is added as needed. Parameters: trait (str): configuration trait B64 from TraitDex """ if not (qb64b or qb64 or qb2): if trait: tag = trait super(Traitor, self).__init__(qb64b=qb64b, qb64=qb64, qb2=qb2, tag=tag, **kwa) if self.trait not in TraitDex: raise InvalidSoftError(f"Invalid trait={self.trait} for Traitor.")
@property def trait(self): """Returns: trait (str): B64 primitive without prepad (strips prepad from soft) Alias for self.tag """ return self.tag
# Versage namedtuple # proto (str): protocol element of Protocols # pvrsn (Versionage): instance protocol version namedtuple (major, minor) ints # gvrsn (Versionage | None): instance genus version namedtuple (major, minor) ints Versage = namedtuple("Versage", "proto pvrsn gvrsn", defaults=(None, ))
[docs] class Verser(Tagger): """ Verser is subclass of Tagger, cryptographic material, for formatted version primitives in Base64. Leverages Tagger support compact special fixed size primitives with non-empty soft part and empty raw part. Verser provides a more compact representation than would be obtained by converting the raw ASCII representation to Base64. Attributes: Inherited Properties: (See Tagger) code (str): hard part of derivation code to indicate cypher suite hard (str): hard part of derivation code. alias for code soft (str): soft part of derivation code fs any. Empty when ss = 0. both (str): hard + soft parts of full text code size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) fullSize (int): full size of primitive raw (bytes): crypto material only. Not derivation code or lead bytes. qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise prefixive (bool): True means identifier prefix derivation code False otherwise special (bool): True when soft is special raw is empty and fixed size composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip tag (str): B64 primitive without prepad (strips prepad from soft) Properties: versage (Versage): named tuple of (proto, pvrsn, gvrsn) Inherited Hidden: (See Tagger) _code (str): value for .code property _soft (str): soft value of full code _raw (bytes): value for .raw property _rawSize(): _leadSize(): _special(): _infil(): creates qb64b from .raw and .code (fully qualified Base64) _binfil(): creates qb2 from .raw and .code (fully qualified Base2) _exfil(): extracts .code and .raw from qb64b (fully qualified Base64) _bexfil(): extracts .code and .raw from qb2 (fully qualified Base2) Hidden: Methods: """
[docs] def __init__(self, qb64b=None, qb64=None, qb2=None, versage=None, proto=Protocols.keri, pvrsn=Vrsn_2_0, gvrsn=None, tag='', **kwa): """ Inherited Parameters: (see Tagger) raw (bytes | bytearray | None): unqualified crypto material usable for crypto operations. code (str): stable (hard) part of derivation code soft (str | bytes): soft part for special codes rize (int | None): raw size in bytes when variable sized material not including lead bytes if any Otherwise None qb64b (bytes | None): fully qualified crypto material Base64 qb64 (str | bytes | None): fully qualified crypto material Base64 qb2 (bytes | None): fully qualified crypto material Base2 strip (bool): True means strip (delete) matter from input stream bytearray after parsing qb64b or qb2. False means do not strip tag (str | bytes): Base64 plain. Prepad is added as needed. Parameters: versage (Versage | None): namedtuple of (proto, pvrsn, gvrsn) proto (str | None): protocol from Protocols pvrsn (Versionage | None): instance protocol version. namedtuple (major, minor) of ints gvrsn (Versionage | None): instance genus version. namedtuple (major, minor) of ints """ if not (qb64b or qb64 or qb2): if versage: proto, pvrsn, gvrsn = versage tag = proto + self.verToB64(pvrsn) if gvrsn: tag += self.verToB64(gvrsn) super(Verser, self).__init__(qb64b=qb64b, qb64=qb64, qb2=qb2, tag=tag, **kwa) if self.code not in (MtrDex.Tag7, MtrDex.Tag10, ): raise InvalidCodeError(f"Invalid code={self.code} for " f"Verser={self.tag}.")
@property def versage(self): """Returns: versage (Versage): named tuple of (proto, pvrsn, gvrsn) """ proto = self.tag[:4] pvrsn = self.b64ToVer(self.tag[4:7]) gvrsn = self.b64ToVer(self.tag[7:10]) if len(self.tag) == 10 else None return Versage(proto=proto, pvrsn=pvrsn, gvrsn=gvrsn)
[docs] @staticmethod def verToB64(version=None, *, text="", major=0, minor=0): """ Converts version to Base64 representation Returns: verB64 (str): Example: Verser.verToB64(verstr = "1.0")) Parameters: version (Versionage): instange of namedtuple Versionage(major=major,minor=minor) text (str): text format of version as dotted decimal "major.minor" major (int): When version is None and verstr is empty then use major minor range [0, 63] for one Base64 character minor (int): When version is None and verstr is empty then use major minor range [0, 4095] for two Base64 characters """ if version: major = version.major minor = version.minor elif text: splits = text.split(".", maxsplit=2) splits = [(int(s) if s else 0) for s in splits] parts = [major, minor] for i in range(2-len(splits),0, -1): # append missing minor and/or major splits.append(parts[-i]) major = splits[0] minor = splits[1] if major < 0 or major > 63 or minor < 0 or minor > 4095: raise ValueError(f"Out of bounds version = {major}.{minor}.") return (f"{intToB64(major)}{intToB64(minor, l=2)}")
[docs] @staticmethod def b64ToVer(b64, *, texted=False): """ Converts Base64 representation of version to Versionage or text dotted decimal format default is Versionage Returns: version (Versionage | str): Example: .b64ToVer("BAA")) Parameters: b64 (str): base64 string of three characters Mmm for Major minor texted (bool): return text format dotted decimal string """ if not Reb64.match(b64.encode("utf-8")): raise ValueError("Invalid Base64.") if texted: return ".".join([f"{b64ToInt(b64[0])}", f"{b64ToInt(b64[1:3])}"]) return Versionage(major=b64ToInt(b64[0]), minor=b64ToInt(b64[1:3]))
[docs] class Texter(Matter): """ Texter is subclass of Matter, cryptographic material, for variable length text strings as bytes not unicode. Unicode strings converted to bytes. Attributes: Inherited Properties: (See Matter) size (int | None): Number of quadlets/triplets of chars/bytes including lead bytes of variable sized material (fs = None). Converted value of the soft part (of len ss) of full derivation code. Otherwise None when not variably sized (fs != None) Properties: text (str): unqaulified text str without CESR code and leader. Inherited Hidden Properties: (See Matter) Methods: Codes: Bytes_L0: str = '4B' # Byte String lead size 0 Bytes_L1: str = '5B' # Byte String lead size 1 Bytes_L2: str = '6B' # Byte String lead size 2 Bytes_Big_L0: str = '7AAB' # Byte String big lead size 0 Bytes_Big_L1: str = '8AAB' # Byte String big lead size 1 Bytes_Big_L2: str = '9AAB' # Byte String big lead size 2 """
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.Bytes_L0, text=None, **kwa): """ Inherited Parameters: (see Matter) raw (bytes|None): of unqualified variable sized text string qb64b (bytes|None): fully qualified text domain of raw/text qb64 (str|bytes|None): fully qualified text domain of raw/text qb2 (bytes|None): fully qualified binary domain of raw/text code (str): hard part of derivation code in text domain Parameters: text (str|bytes|None): unqualified variable sized text string as either same as raw but may be as str, so handles encoding/decoding to/from bytes/str """ if raw is None and qb64b is None and qb64 is None and qb2 is None: if text is None: raise EmptyMaterialError("Missing text string.") if hasattr(text, "encode"): text = text.encode() raw = text super(Texter, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code not in TexDex: raise ValidationError("Invalid code = {} for Texter." "".format(self.code))
@property def text(self): """ Property text: raw as str """ return self.raw.decode()
[docs] class Bexter(Matter): """ Bexter is subclass of Matter, cryptographic material, for variable length strings that only contain Base64 URL safe characters, i.e. Base64 text (bext). When created using the 'bext' paramaeter, the encoded matter in qb64 format in the text domain is more compact than would be the case if the string were passed in as raw bytes. The text is used as is to form the value part of the qb64 version not including the leader. Due to ambiguity that arises from pre-padding bext whose length is a multiple of three with one or more 'A' chars. Any bext that starts with an 'A' and whose length is either a multiple of 3 or 4 may not round trip. Bext with a leading 'A' whose length is a multiple of four may have the leading 'A' stripped when round tripping. Bexter(bext='ABBB').bext == 'BBB' Bexter(bext='BBB').bext == 'BBB' Bexter(bext='ABBB').qb64 == '4AABABBB' == Bexter(bext='BBB').qb64 To avoid this problem, only use for applications of base 64 strings that never start with 'A' Examples: base64 text strings: bext = "" qb64 = '4AAA' bext = "-" qb64 = '6AABAAA-' bext = "-A" qb64 = '5AABAA-A' bext = "-A-" qb64 = '4AABA-A-' bext = "-A-B" qb64 = '4AAB-A-B' Example uses: CESR encoded paths for nested SADs and SAIDs CESR encoded fractionally weighted threshold expressions Attributes: Inherited Properties: (See Matter) Properties: .bext is the Base64 text value, .qb64 with text code and leader removed. Inherited Hidden Properties: (See Matter) Methods: ._rawify(self, bext) Codes: StrB64_L0: str = '4A' # String Base64 Only Leader Size 0 StrB64_L1: str = '5A' # String Base64 Only Leader Size 1 StrB64_L2: str = '6A' # String Base64 Only Leader Size 2 StrB64_Big_L0: str = '7AAA' # String Base64 Only Big Leader Size 0 StrB64_Big_L1: str = '8AAA' # String Base64 Only Big Leader Size 1 StrB64_Big_L2: str = '9AAA' # String Base64 Only Big Leader Size 2 """ @classmethod def _derawify(cls, raw, code): """Returns decoded raw as B64 str aka bext value Returns: bext (str): decoded raw as B64 str aka bext value """ _, _, _, _, ls = cls.Sizes[code] bext = encodeB64(bytes([0] * ls) + raw) ws = 0 if ls == 0 and bext: if bext[0] == ord(b'A'): # strip leading 'A' zero pad ws = 1 else: ws = (ls + 1) % 4 return bext.decode('utf-8')[ws:]
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.StrB64_L0, bext=None, **kwa): """ Inherited Parameters: (see Matter) raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes Parameters: bext is the variable sized Base64 text string """ if raw is None and qb64b is None and qb64 is None and qb2 is None: if bext is None: raise EmptyMaterialError("Missing bext string.") if hasattr(bext, "encode"): bext = bext.encode("utf-8") # convert to bytes if not Reb64.match(bext): raise ValueError("Invalid Base64.") raw = self._rawify(bext) # convert bytes to raw with padding super(Bexter, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa) if self.code not in BexDex: raise ValidationError("Invalid code = {} for Bexter." "".format(self.code))
@staticmethod def _rawify(bext): """Returns raw value equivalent of Base64 text. Suitable for variable sized matter Parameters: bext (bytes): Base64 bytes """ ts = len(bext) % 4 # bext size mod 4 ws = (4 - ts) % 4 # pre conv wad size in chars ls = (3 - ts) % 3 # post conv lead size in bytes base = b'A' * ws + bext # pre pad with wad of zeros in Base64 == 'A' raw = decodeB64(base)[ls:] # convert and remove leader return raw # raw binary equivalent of text @property def bext(self): """ Property bext: Base64 text value portion of qualified b64 str Returns the value portion of .qb64 with text code and leader removed """ return self._derawify(raw=self.raw, code=self.code)
[docs] class Pather(Matter): """Pather is Matter subclass that provides path and route functionality. It provides support for SAD Path language specific functionality. When path parts contain only Base64 characters not including '-', the path will be compactly encoded with special path separator '-' as a variable length StrBase64. Otherwise paths are encoded as variable length Bytes. Pather allows the specification of paths as a list of field parts which will be converted to the compact Base64 URL safe character representation when possible. Paths may be relative or absolute. A special, escape secquence is used to encode relative paths in Base64 Pather can traverse paths that maintain Base64 URL character safety by leveraging the fact that SADs must have static field ordering. Any field label can be replaced by its field ordinal to allow for path specification and traversal for any field labels that contain non-Base64 URL safe characters. Inherited Properties and Attributes: (See Matter class) Properties: path (str): '/' separated path. Absolute paths start with '/'. Relative paths do not. parts (tuple): parts of path, absolute path has zeroth part as empty string. Bare absolute path has zeroth and first part as empty strings. rparts (tuple): parts of path in relative form. No leading '/' in associated path Examples: path = "AAAA" makes path = "/AAAA" path = "AAA/BBB" with relative == True path = "/@AA/BBB" with pathive == False """
[docs] def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, code=MtrDex.StrB64_L0, parts=None, path=None, relative=False, pathive=True, **kwa): """ Inherited Parameters: (see Bexter) raw is bytes of unqualified crypto material usable for crypto operations qb64b is bytes of fully qualified crypto material qb64 is str or bytes of fully qualified crypto material qb2 is bytes of fully qualified crypto material code is str of derivation code index is int of count of attached receipts for CryCntDex codes relative (bool): True means relative paths allowed False means all paths are forced to be absolute pathive (bool): True means must satisfy valid pathed material path False means must only satisfy valid route path Parameters: parts (NonStringIterable): of path parts path (str|bytes|NonStringIterable): as either '/' delimited string or as NonStringIterable of path parts. """ if raw is None and qb64b is None and qb64 is None and qb2 is None: if parts is None and path is None: raise EmptyMaterialError("Need either parts or path") if parts is None: # therefore path must not be None if isNonStringIterable(path): parts = path else: if hasattr(path, 'encode'): path = path.encode() # make bytes, allow relative if b'/' in path: parts = path.split(b'/') # "/".split("/") == ["",""] else: parts = path.split(b'-') parts = [part.encode() if hasattr(part, 'encode') else part for part in parts] bextable = True # TRue means more compact b64 raw, otherwise raw is text for part in parts: if not Repath.match(part): # matches empty if pathive: # pathive so parts MUST satisfy Repath raise InvalidValueError(f"Invalid pathive path {part=}") bextable = False if not relative: if parts and parts[0]: # zeroth part is not empty from path parts.insert(0, b'') # insert empty part forces absolute path elif not parts: # empty parts parts = [b'', b''] # to empty makes bare absolute "/" if bextable: path = b'-'.join(parts) if b'--' in path: # forbid double path separatorstext raise InvalidValueError(f"Non-unitary path separators for path={path}") else: path = b'/'.join(parts) if b'//' in path: # forbid double path separatorstext raise InvalidValueError(f"Non-unitary path separators for path={path}") if bextable: # use StrB64 code code = MtrDex.StrB64_L0 ws = (4 - (len(path) % 4)) % 4 # pre conv wad size in chars if path and path[0] == ord(b'A') and (ws in (0, 1)): # use escape sequence path = b'--' + path raw = Bexter._rawify(path) else: # use Bytes code code = MtrDex.Bytes_L0 raw = path super(Pather, self).__init__(raw=raw, qb64b=qb64b, qb64=qb64, qb2=qb2, code=code, **kwa)
@property def path(self): """Extracts and returns path from .code and .raw Returns: path (str): '/' separated path. Absolute paths start with '/'. Relative paths do not. """ if self.code in BexDex: # bextable path = Bexter._derawify(raw=self.raw, code=self.code) path = path.removeprefix('--') # escape sequence for relative pathive path parts = path.split("-") path = '/'.join(parts) else: path = self.raw.decode() return path @property def parts(self): """Path as a tuple of path parts Returns: parts (tuple): parts of path, absolute path has zeroth part as empty string. Bare absolute path has zeroth and first part as empty strings. """ if self.code in BexDex: # pathive path = Bexter._derawify(raw=self.raw, code=self.code) path = path.removeprefix('--') # escape sequence for relative pathive path parts = path.split("-") else: path = self.raw.decode() parts = path.split("/") return parts @property def rparts(self): """Path as a tuple of path parts but in relative form. Associate path would not have leading '/' Returns: rparts (tuple): parts of path in relative form. No leading '/' in associated path """ if self.code in BexDex: # pathive path = Bexter._derawify(raw=self.raw, code=self.code) path = path.removeprefix('--') # escape sequence for relative pathive path parts = path.split("-") else: path = self.raw.decode() parts = path.split("/") while parts and parts[0] == "": del parts[0] return parts
[docs] def root(self, root): """ Returns a new Pather anchored at new root Returns a new Pather anchoring this path at the new root specified by root. Args: root(Pather): the new root to apply to this path Returns: Pather: new path anchored at root """ parts = list(root.parts) if parts and len(parts) >= 2 and parts[-1] == '': del parts[-1] # remove trailing '/' return Pather(parts=parts + self.rparts)
[docs] def strip(self, root): """ Returns a new Pather with root stipped off the front if it exists Returns a new Pather with root stripped off the front Args: root(Pather): the new root to apply to this path Returns: Pather: new path anchored at root """ if len(root.parts) > len(self.parts): return Pather(parts=self.parts) parts = list(self.parts) try: for i in root.parts: parts.remove(i) except ValueError: return Pather(parts=self.parts) return Pather(parts=parts)
[docs] def startswith(self, pather): """ Returns True if pather.path text is the root of self.path text Parameters: pather (Pather): the path to check against self Returns: bool: True if pather is the root of self """ return self.path.startswith(pather.path)
[docs] def tail(self, serder): """ Recurses thru value following .path and returns terminal value Finds the value at this path and applies the version string rules of the serder to serialize the value at ptr. Parameters: serder(Serder): the versioned dict to in which to resolve .path Returns: bytes: Value at the end of the path """ val = self.resolve(sad=serder.sad) if isinstance(val, str): diger = Diger(qb64=val) return diger.qb64b elif isinstance(val, dict): return dumps(val, serder.kind) elif isinstance(val, list): return dumps(val, serder.kind) else: raise ValueError(f"Bad tail value at {self.rparts} of {serder.ked}")
[docs] def resolve(self, sad): """ Recurses thru sad dict following self.parts Parameters: sad(dict or list): the next component Returns: Value at the end of the path """ return self._resolve(sad, self.rparts) # use relative parts
def _resolve(self, val, parts): """ Recurses thru value following ptr Parameters: val(Optional(dict,list)): the next component parts(list): list of path parts Returns: Value at the end of the chain """ if len(parts) == 0: return val idx = parts.pop(0) if isinstance(val, dict): if idx.isdigit(): i = int(idx) keys = list(val) if i >= len(keys): raise KeyError(f"invalid dict pointer index {i} for keys {keys}") cur = val[list(val)[i]] elif idx == "": return val else: cur = val[idx] elif isinstance(val, list): i = int(idx) if i >= len(val): raise KeyError(f"invalid array pointer index {i} for array {val}") cur = val[i] else: raise KeyError("invalid traversal type") return self._resolve(cur, parts)
[docs] class Labeler(Matter): """ Labeler is subclass of Matter for CESR native field map labels and/or generic textual field values. Labeler auto sizes the instance code to minimize the total encoded size of associated field label or textual field value. Attributes: Inherited Properties: (See Matter) Properties: label (str): base value without encoding Inherited Hidden: (See Matter) Hidden: Methods: """
[docs] def __init__(self, label=None, text=None, raw=None, code=None, soft=None, **kwa): """ Inherited Parameters: (see Matter) Parameters: label (str|bytes): base value before encoding as label A valid label must match rules for attribute name text (str|bytes): base value for encoding as text, Can be any str or bytes """ if label: # label can't be empty string '' since '' is not Reatt if hasattr(label, "encode"): # make label bytes label = label.encode() if not Reatt.match(label): raise InvalidValueError(f"Invalid {label=}") # candidate for Base64 compact encoding try: code = Tagger._codify(tag=label) soft = label except InvalidSoftError as ex: # too big ws = (4 - (len(label) % 4)) % 4 # pre conv wad size in chars if label[0] == ord(b'A') and (ws in (0, 1)): # use excape code label = b'-' + label code = LabelDex.StrB64_L0 raw = Bexter._rawify(label) #else: # use Texter code since ambiguity if starts with 'A' and ws in (0,1) #code = LabelDex.Bytes_L0 #raw = label elif text is not None: # text can be empty string since '' is Reb64 if hasattr(text, "encode"): # make text bytes text = text.encode() if Reb64.match(text): # try Base64 compact encoding includes '' if len(text) == 0: code = LabelDex.Empty raw = text else: try: code = Tagger._codify(tag=text) soft = text except InvalidSoftError as ex: # too big ws = (4 - (len(text) % 4)) % 4 # pre conv wad size in chars if text[0] != ord(b'A') or not (ws in (0, 1)): # use Bexter code # can't use escape code here because Reb64 allows all B64 chars. code = LabelDex.StrB64_L0 raw = Bexter._rawify(text) else: # use Texter code since ambiguity if starts with 'A' and ws in (0,1) code = LabelDex.Bytes_L0 raw = text else: # not Reb64 if len(text) == 1: code = LabelDex.Label1 elif len(text) == 2: code = LabelDex.Label2 else: code = LabelDex.Bytes_L0 raw = text super(Labeler, self).__init__(raw=raw, code=code, soft=soft, **kwa) if self.code not in LabelDex: raise InvalidCodeError(f"Invalid code={self.code} for Labeler.")
@property def label(self): """Extracts and returns label from .code and .soft or .code and .raw Returns: label (str): base value without encoding. Must match rules for attribute name """ if self.code in TagDex: # tag label = self.soft # soft part of code elif self.code in BexDex: # bext label = Bexter._derawify(raw=self.raw, code=self.code) # derawify if label[0] == '-': # excape code label = label[1:] # remove excape else: label = self.raw.decode() # everything else is just raw as str if not Reatt.match(label.encode()): raise InvalidValueError(f"Invalid {label=}") return label @property def text(self): """Extracts and returns text from .code and .soft or .code and .raw Returns: text (str): base value without encoding """ if self.code in TagDex: # tag return self.soft # soft part of code if self.code in BexDex: # bext label = Bexter._derawify(raw=self.raw, code=self.code) # derawify if label[0] == '-': # excape code see if Reatt if Reatt.match(label[1:].encode()): # so must have been escaped label = label[1:] # remove excape return label return self.raw.decode() # everything else is just raw as str
[docs] class Verfer(Matter): """Verfer is Matter subclass with method to verify signature of serialization using the .raw as verifier key and .code for signature cipher suite. See Matter for inherited attributes and properties: Attributes: Properties: Methods: verify: verifies signature """
[docs] def __init__(self, **kwa): """ Assign verification cipher suite function to ._verify """ super(Verfer, self).__init__(**kwa) if self.code in [MtrDex.Ed25519N, MtrDex.Ed25519]: self._verify = self._ed25519 elif self.code in [MtrDex.ECDSA_256r1N, MtrDex.ECDSA_256r1]: self._verify = self._secp256r1 elif self.code in [MtrDex.ECDSA_256k1N, MtrDex.ECDSA_256k1]: self._verify = self._secp256k1 else: raise ValueError("Unsupported code = {} for verifier.".format(self.code))
[docs] def verify(self, sig, ser): """ Returns True if bytes signature sig verifies on bytes serialization ser using .raw as verifier public key for ._verify cipher suite determined by .code Parameters: sig is bytes signature ser is bytes serialization """ return (self._verify(sig=sig, ser=ser, key=self.raw))
@staticmethod def _ed25519(sig, ser, key): """ Returns True if verified False otherwise Verify Ed25519 sig on ser using key Parameters: sig is bytes signature ser is bytes serialization key is bytes public key """ try: # verify returns None if valid else raises ValueError pysodium.crypto_sign_verify_detached(sig, ser, key) except Exception as ex: return False return True @staticmethod def _secp256r1(sig, ser, key): """ Returns True if verified False otherwise Verify secp256r1 sig on ser using key Parameters: sig is bytes signature ser is bytes serialization key is bytes public key """ verkey = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), key) r = int.from_bytes(sig[:32], "big") s = int.from_bytes(sig[32:], "big") der = utils.encode_dss_signature(r, s) try: verkey.verify(der, ser, ec.ECDSA(hashes.SHA256())) return True except exceptions.InvalidSignature: return False @staticmethod def _secp256k1(sig, ser, key): """ Returns True if verified False otherwise Verify secp256k1 sig on ser using key Parameters: sig is bytes signature ser is bytes serialization key is bytes public key """ verkey = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), key) r = int.from_bytes(sig[:32], "big") s = int.from_bytes(sig[32:], "big") der = utils.encode_dss_signature(r, s) try: verkey.verify(der, ser, ec.ECDSA(hashes.SHA256())) return True except exceptions.InvalidSignature: return False
[docs] class Cigar(Matter): """ Cigar is Matter subclass holding a nonindexed signature with verfer property. From Matter .raw is signature and .code is signature cipher suite Adds .verfer property to hold Verfer instance of associated verifier public key Verfer's .raw as verifier key and .code is verifier cipher suite. See Matter for inherited attributes and properties: Attributes: Properties: (Inherited) .code is str derivation code to indicate cypher suite .size is size (int): number of quadlets when variable sized material besides full derivation code else None .raw is bytes crypto material only without code .qb64 is str in Base64 fully qualified with derivation code + crypto mat .qb64b is bytes in Base64 fully qualified with derivation code + crypto mat .qb2 is bytes in binary with derivation code + crypto material .transferable is Boolean, True when transferable derivation code False otherwise .digestive is Boolean, True when digest derivation code False otherwise Properties: .verfer is verfer of public key used to verify signature Hidden: ._code is str value for .code property ._size is int value for .size property ._raw is bytes value for .raw property ._infil is method to compute fully qualified Base64 from .raw and .code ._exfil is method to extract .code and .raw from fully qualified Base64 Methods: Hidden: ._pad is method to compute .pad property ._code is str value for .code property ._raw is bytes value for .raw property ._index is int value for .index property ._infil is method to compute fully qualified Base64 from .raw and .code ._exfil is method to extract .code and .raw from fully qualified Base64 """
[docs] def __init__(self, verfer=None, **kwa): """ Assign verfer to ._verfer attribute """ super(Cigar, self).__init__(**kwa) self._verfer = verfer
@property def verfer(self): """ Property verfer: Returns Verfer instance Assumes ._verfer is correctly assigned """ return self._verfer @verfer.setter def verfer(self, verfer): """ verfer property setter """ self._verfer = verfer
[docs] class Diger(Matter): """Diger is Matter subclass with method to verify digest of serialization See Matter for inherited attributes and properties: Methods: verify: verifies digest given ser compare: compares provide digest given ser to this digest of ser. enables digest agility of different digest algos to compare. """ # Maps digest codes to Digestages of algorithms for computing digest. # Should be based on the same set of codes as in DigestCodex # so Matter.digestive property works. # Use unit tests to ensure codex elements sets match Digests = { DigDex.Blake3_256: Digestage(klas=blake3.blake3, size=None, length=None), DigDex.Blake2b_256: Digestage(klas=hashlib.blake2b, size=32, length=None), DigDex.Blake2s_256: Digestage(klas=hashlib.blake2s, size=None, length=None), DigDex.SHA3_256: Digestage(klas=hashlib.sha3_256, size=None, length=None), DigDex.SHA2_256: Digestage(klas=hashlib.sha256, size=None, length=None), DigDex.Blake3_512: Digestage(klas=blake3.blake3, size=None, length=64), DigDex.Blake2b_512: Digestage(klas=hashlib.blake2b, size=None, length=None), DigDex.SHA3_512: Digestage(klas=hashlib.sha3_512, size=None, length=None), DigDex.SHA2_512: Digestage(klas=hashlib.sha512, size=None, length=None), } @classmethod def _digest(cls, ser, code=DigDex.Blake3_256): """Returns raw digest of ser using digest algorithm given by code Parameters: ser (bytes): serialization from which raw digest is computed code (str): derivation code used to lookup digest algorithm """ if code not in cls.Digests: raise InvalidCodeError(f"Unsupported Digest {code=}.") klas, size, length = cls.Digests[code] # digest algo size & length ikwa = dict(digest_size=size) if size else dict() # opt digest size dkwa = dict(length=length) if length else dict() # opt digest length raw = klas(ser, **ikwa).digest(**dkwa) return (raw)
[docs] def __init__(self, raw=None, code=DigDex.Blake3_256, ser=None, strict=True, **kwa): """Initialize attributes Inherited Parameters: See Matter Parameters: ser (bytes): serialization from which raw is computed if not raw strict (bool): True means enforce code must be in DigDex False means do not enfoce code in DigDex this allows subclasses to enforce different codex """ try: super(Diger, self).__init__(raw=raw, code=code, **kwa) except EmptyMaterialError as ex: if not ser: raise ex raw = self._digest(ser, code=code) super(Diger, self).__init__(raw=raw, code=code, **kwa) if strict and self.code not in DigDex: raise InvalidCodeError(f"Unsupported Digest {code=}.")
[docs] def verify(self, ser): """ Returns True if raw digest of ser bytes (serialization) matches .raw using .raw as reference digest for digest algorithm determined by .code Parameters: ser (bytes): serialization to be digested and compared to .raw """ return (self._digest(ser=ser, code=self.code) == self.raw)
[docs] def compare(self, ser, dig=None, diger=None): """ Returns True if dig and .qb64 or .qb64b match or if both .raw and dig are valid digests of ser Otherwise returns False Parameters: ser is bytes serialization dig is qb64b or qb64 digest of ser to compare with self diger is Diger instance of digest of ser to compare with self if both supplied dig takes precedence If both match then as optimization returns True and does not verify either as digest of ser Else If both have same code but do not match then as optimization returns False and does not verify if either is digest of ser Else recalcs both digests using each one's code to verify they they are both digests of ser regardless of matching codes. """ if dig is not None: if hasattr(dig, "encode"): dig = dig.encode('utf-8') # makes bytes if dig == self.qb64b: # matching return True diger = Diger(qb64b=dig) # extract code elif diger is not None: if diger.qb64b == self.qb64b: return True else: raise ValueError("Both dig and diger may not be None.") if diger.code == self.code: # digest not match but same code return False if diger.verify(ser=ser) and self.verify(ser=ser): # both verify on ser return True return (False)
[docs] class Prefixer(Matter): """ Prefixer is Matter subclass for autonomic identifier AID prefix Attributes: Inherited Properties: (see Matter) Properties: Methods: Hidden: """
[docs] def __init__(self, **kwa): """Checks for .code in PreDex so valid prefixive code Inherited Parameters: See Matter """ super(Prefixer, self).__init__(**kwa) if self.code not in PreDex: raise InvalidCodeError(f"Invalid prefixer code = {self.code}.")
[docs] class Noncer(Diger): """Noncer is Diger subclass for UUIDs as salty nonces or UUIDs as digest deterministically derived from salty nonces Attributes: Inherited Properties: (see Diger) Properties: nonce (str): round trippable nonce value that is empty string when nonce is empty. Otherwise qb64 of nonce. nonceb (bytes): round trippable nonce value that is empty string when nonce is empty. Otherwise qb64 of nonce. Methods: Hidden: """
[docs] def __init__(self, raw=None, code=NonceDex.Salt_128, qb64b=None, nonce=None, **kwa): """Checks for .code in NonceDex so valid noncive code Inherited Parameters: (see Diger) Parameters: nonce (str|bytes|None): round trippable nonce value with nonce property that accounts for empty nonce indicated by empty string. Otherwise nonce is qb64 of nonce value """ try: super(Noncer, self).__init__(raw=raw, qb64b=qb64b, code=code, strict=False, **kwa) except EmptyMaterialError as ex: if nonce is not None: if hasattr(nonce, "encode"): nonce = nonce.encode() if nonce == b'': raw = nonce code = NonceDex.Empty qb64b = None else: qb64b = nonce # should be qb64b of non-empty nonce raw = None else: if code == NonceDex.Salt_128: raw = pysodium.randombytes(pysodium.crypto_pwhash_SALTBYTES) elif code == NonceDex.Salt_256: raw = pysodium.randombytes(pysodium.crypto_sign_SEEDBYTES) else: raise super(Noncer, self).__init__(raw=raw, code=code, qb64b=qb64b, strict=False, **kwa) if self.code not in NonceDex: raise InvalidCodeError(f"Invalid noncer code = {self.code}.")
@property def nonce(self): """Property nonce: Returns: nonce (str): Either empty str when Nonce is NonceDex.Empty or qb64 of nonce primitive otherwise """ if self.code == NonceDex.Empty: return '' return self.qb64 @property def nonceb(self): """Property nonceb: Returns: nonce (bytes): Either empty bytes when Nonce is NonceDex.Empty or qb64b of nonce primitive otherwise """ if self.code == NonceDex.Empty: return b'' return self.qb64b
[docs] class Saider(Matter): """Saider is Matter subclass for self-addressing identifier prefix using derivation as determined by code from ked Properties: (inherited) code (str): derivation code to indicate cypher suite size (int): number of quadlets when variable sized material besides full derivation code else None raw (bytes): crypto material only without code qb64 (str): Base64 fully qualified with derivation code + crypto mat qb64b (bytes): Base64 fully qualified with derivation code + crypto mat qb2 (bytes): binary with derivation code + crypto material transferable (bool): True means transferable derivation code False otherwise digestive (bool): True means digest derivation code False otherwise Hidden: _code (str): value for .code property _size (int): value for .size property _raw (bytes): value for .raw property _infil (types.MethodType): creates qb64b from .raw and .code (fully qualified Base64) _exfil (types.MethodType): extracts .code and .raw from qb64b (fully qualified Base64) _derive (types.MethodType): derives said (.qb64 ) _verify (types.MethodType): verifies said ((.qb64 ) against a given sad """ Dummy = "#" # dummy spaceholder char for said. Must not be a valid Base64 char
[docs] def __init__(self, raw=None, *, code=None, sad=None, kind=None, label=Saids.d, ignore=None, **kwa): """ See Matter.__init__ for inherited parameters Parameters: sad (dict): self addressed data to serialize and inject said kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json label (str): Saidage value as said field label ignore (list): fields to ignore when generating SAID """ try: # when raw and code are both provided super(Saider, self).__init__(raw=raw, code=code, **kwa) except EmptyMaterialError as ex: # raw or code missing if not sad or label not in sad: raise ex # need sad with label field to calculate raw or qb64 if not code: if sad[label]: # no code but sad[label] not empty # attempt to get code from said in sad super(Saider, self).__init__(qb64=sad[label], code=code, **kwa) code = self._code else: # use default code code = MtrDex.Blake3_256 if code not in DigDex: # need valid code raise ValueError("Unsupported digest code = {}.".format(code)) # make copy of sad to derive said raw bytes and new sad # need new sad because sets sad[label] and sad['v'] fields raw, sad = self.derive(sad=dict(sad), code=code, kind=kind, label=label, ignore=ignore) super(Saider, self).__init__(raw=raw, code=code, **kwa) if not self.digestive: raise ValueError("Unsupported digest code = {}." "".format(self.code))
@classmethod def _serialize(clas, sad: dict, kind: str = None): """ Serialize sad with serialization kind if provided else use use embedded 'v', version string if provided else use default Serials.json Returns: ser (bytes): raw serialization of sad Parameters: sad (dict): serializable dict kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json """ knd = Kinds.json if 'v' in sad: # versioned sad _, _, knd, _, _ = deversify(sad['v']) if not kind: # match logic of Serder for kind kind = knd return dumps(sad, kind=kind)
[docs] @classmethod def saidify(clas, sad: dict, *, code: str = MtrDex.Blake3_256, kind: str = None, label: str = Saids.d, ignore: list = None, **kwa): """ Derives said from sad and injects it into copy of sad and said and injected sad Returns: result (tuple): of the form (saider, sad) where saider is Saider instance generated from sad using code and sad is copy of parameter sad but with its label id field filled in with generated said from saider Parameters: sad (dict): serializable dict code (str): digest type code from DigDex kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json label (str): Saidage value as said field label in which to inject said ignore (list): fields to ignore when generating SAID """ if label not in sad: raise KeyError("Missing id field labeled={} in sad.".format(label)) raw, sad = clas._derive(sad=sad, code=code, kind=kind, label=label, ignore=ignore) saider = clas(raw=raw, code=code, kind=kind, label=label, ignore=ignore, **kwa) sad[label] = saider.qb64 return saider, sad
@classmethod def _derive(clas, sad: dict, *, code: str = MtrDex.Blake3_256, kind: str = None, label: str = Saids.d, ignore: list = None): """ Derives raw said from sad with .Dummy filled sad[label] Returns: raw (bytes): raw said from sad with dummy filled label id field Parameters: sad (dict): self addressed data to be injected with dummy and serialized code (str): digest type code from DigDex kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json label (str): Saidage value as said field label in which to inject dummy ignore (list): fields to ignore when generating SAID """ if code not in DigDex: raise ValueError(f"Unsupported digest {code=}.") sad = dict(sad) # make shallow copy so don't clobber original sad # fill id field denoted by label with dummy chars to get size correct sad[label] = clas.Dummy * Matter.Sizes[code].fs if 'v' in sad: # if versioned then need to set size in version string raw, proto, kind, sad, version = sizeify(ked=sad, kind=kind) ser = dict(sad) if ignore: # delete ignore fields in said calculation from ser dict for f in ignore: del ser[f] cpa = clas._serialize(ser, kind=kind) # serialize ser return (Diger._digest(ser=cpa, code=code), sad) # raw digest and sad
[docs] def derive(self, sad, code=None, **kwa): """ Returns: result (tuple): (raw, sad) raw said as derived from serialized dict and modified sad during derivation. Parameters: sad (dict): self addressed data to be serialized code (str): digest type code from DigDex. kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json label (str): Saidage value of said field labelin which to inject dummy """ code = code if code is not None else self.code return self._derive(sad=sad, code=code, **kwa)
[docs] def verify(self, sad, *, prefixed=False, versioned=True, code=None, kind=None, label=Saids.d, ignore=None, **kwa): """ Returns: result (bool): True means derivation from sad with dummy label field value replacement for ._code matches .qb64. False otherwise If prefixed is True then also validates that label field of provided sad also matches .qb64. False otherwise If versioned is True and provided sad includes version field 'v' then also validates that version field 'v' of provided sad matches the version field of modified sad that results from the derivation process. The size chars in the version field are set to the size of the sad during derivation. False otherwise. Parameters: sad (dict): self addressed data to be serialized prefixed (bool): True means also verify if labeled field in sad matches own .qb64 versioned (bool): code (str): digest type code from DigDex. kind (str): serialization algorithm of sad, one of Serials used to override that given by 'v' field if any in sad otherwise default is Serials.json label (str): Saidage value of said field label in which to inject dummy ignore (list): fields to ignore when generating SAID """ try: # override ensure code is self.code raw, dsad = self._derive(sad=sad, code=self.code, kind=kind, label=label, ignore=ignore) saider = Saider(raw=raw, code=self.code, ignore=ignore, **kwa) if self.qb64b != saider.qb64b: return False # not match .qb64b if 'v' in sad and versioned: if sad['v'] != dsad['v']: return False # version fields not match if prefixed and sad[label] != self.qb64: # check label field return False # label id field not match .qb64 except Exception as ex: return False return True
[docs] class Sadder: """ Sadder is self addressed data (SAD) serializer-deserializer class Instance creation of a Sadder does not verifiy it .said property it merely extracts it. In order to ensure Sadder instance has a verified .said then must call .saider.verify(sad=self.ked) Has the following public properties: Properties: raw (bytes): of serialized event only ked (dict): self addressed data dict kind (str): serialization kind coring.Serials such as JSON, CBOR, MGPK, CESR size (int): number of bytes in serialization version (Versionage): protocol version (Major, Minor) proto (str): Protocolage value as protocol identifier such as KERI, ACDC label (str): Saidage value as said field label saider (Saider): of SAID of this SAD .ked['d'] if present said (str): SAID of .saider qb64 saidb (bytes): SAID of .saider qb64b pretty (str): Pretty JSON of this SAD Hidden Attributes: ._raw is bytes of serialized event only ._ked is key event dict ._kind is serialization kind string value (see namedtuple coring.Serials) supported kinds are 'json', 'cbor', 'msgpack', 'binary' ._size is int of number of bytes in serialed event only ._version is Versionage instance of event version ._proto (str): Protocolage value as protocol type identifier ._saider (Saider): instance for this Sadder's SAID Note: loads and jumps of json use str whereas cbor and msgpack use bytes """ MaxVSOffset = 12 SmellSize = MaxVSOffset + MAXVERFULLSPAN # min buffer size to inhale
[docs] def __init__(self, raw=b'', ked=None, sad=None, kind=None, saidify=False, code=MtrDex.Blake3_256): """ Deserialize if raw provided does not verify assumes embedded said is valid Serialize if ked provided but not raw verifies if verify is True? When serializing if kind provided then use kind instead of field in ked Parameters: raw (bytes): serialized event ked is key event dict or None if None its deserialized from raw kind is serialization kind string value or None (see namedtuple coring.Serials) supported kinds are 'json', 'cbor', 'msgpack', 'binary' if kind is None then its extracted from ked or raw saidify (bool): True means compute said for ked code is .diger default digest code for computing said .saider """ self._code = code # need default code for .saider if raw: # deserialize raw using property setter self.raw = raw # raw property setter does the deserialization elif ked: # serialize ked using property setter #ToDo when pass in ked and saidify True then compute said self._kind = kind self.ked = ked # ked property setter does the serialization elif sad: # ToDo do we need this or should we be using ked above with saidify flag self._clone(sad=sad) # copy fields from sad else: raise ValueError("Improper initialization need sad, raw or ked.")
def _clone(self, sad): """ copy hidden attributes from sad """ self._raw = sad.raw self._ked = sad.ked self._kind = sad.kind self._size = sad.size self._version = sad.version self._proto = sad.proto self._saider = sad.saider def _inhale(self, raw): """ Parses serilized event ser of serialization kind and assigns to instance attributes. Parameters: raw is bytes of serialized event kind is str of raw serialization kind (see namedtuple Serials) size is int size of raw to be deserialized Note: loads and jumps of json use str whereas cbor and msgpack use bytes """ proto, vrsn, kind, size, _ = smell(raw) if vrsn != Version: raise VersionError("Unsupported version = {}.{}, expected {}." "".format(vrsn.major, vrsn.minor, Version)) ked = loads(raw=raw, size=size, kind=kind) return ked, proto, kind, vrsn, size def _exhale(self, ked, kind=None): """ Returns sizeify(ked, kind) From sizeify Returns tuple of (raw, proto, kind, ked, version) where: raw (str): serialized event as bytes of kind proto (str): protocol type as value of Protocolage kind (str): serialzation kind as value of Serialage ked (dict): key event dict or sad dict version (Versionage): instance Parameters: ked (dict): key event dict or sad dict kind (str): value of Serials serialization kind. When not provided use Assumes only supports Version """ return sizeify(ked=ked, kind=kind)
[docs] def compare(self, said=None): """ Returns True if said and either .saider.qb64 or .saider.qb64b match via string equality == Convenience method to allow comparison of own .saider digest self.raw with some other purported said of self.raw Parameters: said is qb64b or qb64 SAID of ser to compare with .said """ if said is not None: if hasattr(said, "encode"): said = said.encode('utf-8') # makes bytes return said == self.saidb # matching else: raise ValueError("Both said and saider may not be None.")
@property def raw(self): """ raw property getter """ return self._raw @raw.setter def raw(self, raw): """ raw property setter """ ked, proto, kind, version, size = self._inhale(raw=raw) self._raw = bytes(raw[:size]) # crypto ops require bytes not bytearray self._ked = ked self._proto = proto self._kind = kind self._version = version self._size = size self._saider = Saider(qb64=ked["d"], code=self._code) @property def ked(self): """ ked property getter""" return self._ked @ked.setter def ked(self, ked): """ ked property setter assumes ._kind """ raw, proto, kind, ked, version = self._exhale(ked=ked, kind=self._kind) size = len(raw) self._raw = raw[:size] self._ked = ked self._proto = proto self._kind = kind self._size = size self._version = version self._saider = Saider(qb64=ked["d"], code=self._code) @property def kind(self): """ kind property getter""" return self._kind @kind.setter def kind(self, kind): """ kind property setter Assumes ._ked. Serialization kind. """ raw, proto, kind, ked, version = self._exhale(ked=self._ked, kind=kind) size = len(raw) self._raw = raw[:size] self._proto = proto self._ked = ked self._kind = kind self._size = size self._version = version self._saider = Saider(qb64=ked["d"], code=self._code) @property def size(self): """ size property getter""" return self._size @property def version(self): """ version property getter Returns: (Versionage): """ return self._version @property def proto(self): """ proto property getter protocol identifier type value of Protocolage such as 'KERI' or 'ACDC' Returns: (str): Protocolage value as protocol type """ return self._proto @property def saider(self): """ Returns Diger of digest of self.raw diger (digest material) property getter """ return self._saider @property def said(self): """ Returns str qb64 of .ked["d"] (said when ked is SAD) said (self-addressing identifier) property getter """ return self.saider.qb64 @property def saidb(self): """ Returns bytes qb64b of .ked["d"] (said when ked is SAD) said (self-addressing identifier) property getter """ return self.saider.qb64b
[docs] def pretty(self, *, size=1024): """ Returns str JSON of .ked with pretty formatting ToDo: add default size limit on pretty when used for syslog UDP MCU like 1024 for ogler.logger """ return json.dumps(self.ked, indent=1)[:size if size is not None else None]
[docs] class Tholder: """ Tholder is KERI Signing Threshold Satisfaction class .satisfy method evaluates satisfaction based on ordered list of indices of verified signatures where indices correspond to offsets in key list of associated signatures. Has the following public properties: Properties: .weighted is Boolean True if fractional weighted threshold False if numeric .size is int of minimum size of keys list when weighted is size of keys list when unweighted is size of int thold since don't have anyway to know size of keys list in this case .limen is qualified b64b signing threshold suitable for CESR serialization. either Number.qb64b or Bexter.qb64b. The b64 portion of limen with code stripped (Bexter.bext) of [["1/2", "1/2", "1/4", "1/4", "1/4"], ["1", "1"]] is '1s2c1s2c1s4c1s4c1s4a1c1' basically slash is 's', comma is 'c', ANDed clauses are delimited by 'a'. Each clause top level weight may be optionally a weighted set of weights delimited by 'k' for the weight on the set and 'v' for the weights in the set. [[{'1/3': ['1/2', '1/2', '1/2']}, '1/2', {'1/2': ['1', '1']}], ['1/2', {'1/2': ['1', '1']}]] b'4AAKA1s3k1s2v1s2v1s2c1s2c1s2k1v1a1s2c1s2k1v1' .sith is original signing threshold suitable for value to be serialized as json, cbor, mgpk in key event message as either: non-negative hex number str or list of str rational number fractions >= 0 and <= 1 or list of list of str rational number fractions >= 0 and <= 1 list of list of weighted map of weights .thold is parsed signing threshold suitable for calculating satisfaction. either as int or list of Fractions .num is int signing threshold when not ._weighted Methods: .satisfy returns bool, True means list of verified signature key indices satisfies the threshold, False otherwise. Static Methods: weight (str): converts weight str expression into either int or Fraction else raises ValueError must satisfy 0 <= w <= 1 Ensures strict proper rational number fraction of ints or 0 or 1 Hidden: ._weighted is Boolean, True if fractional weighted threshold False if numeric ._size is int minimum size of of keys list ._sith is signing threshold for .sith property ._thold is signing threshold for .thold propery ._bexter is Bexter instance of weighted signing threshold or None ._number is Number instance of integer threshold or None ._satisfy is method reference of threshold specified verification method ._satisfy_numeric is numeric threshold verification method ._satisfy_weighted is fractional weighted threshold verification method """
[docs] def __init__(self, *, thold=None , limen=None, sith=None, **kwa): """ Accepts signing threshold in various forms so that may output correct forms for serialization and/or calculation of satisfaction. The thold representation is meant to accept thresholds from computable expressions for satisfaction of a threshold The limen representation is meant to parse threshold expressions from CESR serializations of key event message fields or attachments. Limen is passed to Matter to process and accepts all Matter kwa including strip which is useful when extracting from a CESR stream. The sith representation is meant to parse threhold expressions from deserializations of JSON, CBOR, or MGPK key event message fields or the command line or configuration files. Parameters: thold is signing threshold (current or next) is suitable for computing the satisfaction of a threshold and is expressed as either: int of threshold number (M of N) fractional weight clauses which may be expressed as either: sequence of either Fractions or tuples of Fraction and sequence of Fractions sequence of sequence of either Fractions or tuples of Fraction and sequence of Fractions limen is qualified signing threshold (current or next) expressed as either: Number.qb64 or .qb64b of integer threshold or Bexter.qb64 or .qb64b of fractional weight clauses which may be either: Base64 delimited clauses of fractions Base64 delimited clauses of fractions sith is signing threshold (current or next) expressed as either: non-negative int of threshold number (M-of-N threshold) next threshold may be zero non-negative hex string of threshold number (M-of-N threshold) next threshold may be zero fractional weight clauses which may be expressed as either: sequence of rational number fraction strings >= 0 and <= 1 sequence of either rational number fraction strings >= 0 and <= 1 or map with key rational number string and value as sequence of rational number fraction strings rational number fraction string sequence of sequences of rational number fraction strings >= 0 and <= 1 sequence of sequnces of either rational number fraction strings or map with key rational number fraction string with value sequence of rationaly number fraction strings JSON serialized str of the above: """ if thold is not None: self._processThold(thold=thold) elif limen is not None: self._processLimen(limen=limen, **kwa) # kwa for strip elif sith is not None: if isinstance(sith, str) and not sith: # empty str raise EmptyMaterialError("Empty threshold expression.") self._processSith(sith=sith) else: raise EmptyMaterialError("Missing threshold expression.")
@property def weighted(self): """ weighted property getter """ return self._weighted @property def thold(self): """ thold property getter """ return self._thold @property def size(self): """ size property getter """ return self._size @property def limen(self): """ limen property getter """ return self._bexter.qb64b if self._weighted else self._number.qb64b @property def sith(self): """ sith property getter """ # make sith expression of thold if self.weighted: sith = [] for c in self.thold: clause = [] for e in c: if isinstance(e, tuple): f = e[0] k = f"{f.numerator}/{f.denominator}" if (0 < f < 1) else f"{int(f)}" v = [f"{f.numerator}/{f.denominator}" if (0 < f < 1) else f"{int(f)}" for f in e[1]] clause.append({k: v}) else: f = e clause.append(f"{f.numerator}/{f.denominator}" if (0 < f < 1) else f"{int(f)}") sith.append(clause) #sith = [[f"{f.numerator}/{f.denominator}" if (0 < f < 1) else f"{int(f)}" #for f in clause] #for clause in self.thold] if len(sith) == 1: sith = sith[0] # simplify list of one clause to clause else: sith = f"{self.thold:x}" return sith @property def json(self): """Returns json serialization of sith expression Essentially JSON list of lists of strings """ return json.dumps(self.sith) @property def num(self): """ sith property getter """ return self.thold if not self._weighted else None def _processThold(self, thold: int | Sequence): """Process thold input Parameters: thold (int | Sequence): computable thold expression """ if isinstance(thold, int): self._processUnweighted(thold=thold) else: self._processWeighted(thold=thold) def _processLimen(self, limen: str | bytes, **kwa): """Process limen input Parameters: limen (str): CESR encoded qb64 threshold (weighted or unweighted) """ matter = Matter(qb64b=limen, **kwa) # kwa for strip of stream if matter.code in NumDex: number = Number(raw=matter.raw, code=matter.code, **kwa) self._processUnweighted(thold=number.num) elif matter.code in BexDex: # Convert to fractional thold expression bexter = Bexter(raw=matter.raw, code=matter.code, **kwa) t = bexter.bext.replace('s', '/') # get clauses clauses = [clause.split('c') for clause in t.split('a')] thold = [] for c in clauses: clause = [] for e in c: k, s, v = e.partition("k") if s: #not empty clause.append((self.weight(k), [self.weight(w) for w in v.split("v")])) else: clause.append(self.weight(k)) thold.append(clause) self._processWeighted(thold=thold) else: raise InvalidCodeError(f"Invalid code for limen = {matter.code}.") def _processSith(self, sith: int | str | Sequence): """ Process attributes for fractionall weighted threshold sith Parameters: sith is signing threshold (current or next) expressed as either: non-negative int of threshold number (M-of-N threshold) next threshold may be zero non-negative hex string of threshold number (M-of-N threshold) next threshold may be zero fractional weight clauses which may be expressed as either: an sequence of rational number fraction weight str or int str each denoted w where 0 <= w <= 1 an sequence of sequences of rational number fraction weight or int str each denoted w where 0 <= w <= 1>= 0 JSON serialized str of either: list of rational number fraction weight strings each denoted w where 0 <= w <= 1 list of lists of rational number fraction weight strings each denoted w where 0 <= w <= 1 when any w is 0 or 1 then representation is 0 or 1 not 0/1 or 1/1 """ if isinstance(sith, int): self._processUnweighted(thold=sith) elif isinstance(sith, str) and '[' not in sith: self._processUnweighted(thold=int(sith, 16)) else: # assumes sequence of weights or sequence of sequence of weights if isinstance(sith, str): # json of weighted sith from cli sith = json.loads(sith) # deserialize if not sith: # empty or None raise ValueError(f"Empty weight list = {sith}.") # is it non str sequence of sequences? or non str sequnce of strs? # must test for emply mask because all([]) == True mask = [isNonStringSequence(c) for c in sith] # check each element if mask and not all(mask): # not empty and not sequence of sequenes sith = [sith] # attempt to make sequnce of sequqnces of strs for c in sith: # get each clause # each element of a clause must be a str or dict mask = [(isinstance(w, str) or isinstance(w, Mapping)) for w in c] if mask and not all(mask): # not empty and not sequence of str or dicts raise ValueError(f"Invalid sith = {sith} some weights in" f"clause {c} are non string.") # replace weight str expression, int str or fractional strings with # int or fraction as appropriate. thold = [] for c in sith: # convert string fractions to Fractions # append list of where each element is either bare weight or # single key map with value as list of weights # each weight is converted from its str expression clause = [] for e in c: # each element of clause c if isinstance(e, Mapping): if len(e) != 1: raise ValueError(f"Invalid sith = {sith} nested " f"weight map {e} in clause {c} " f" not single key value.") k = list(e)[0] # zeroth key is used # convert to tuple of (weight, [list of weights]) clause.append((self.weight(k), [self.weight(w) for w in e[k]])) else: clause.append(self.weight(e)) thold.append(clause) self._processWeighted(thold=thold) def _processUnweighted(self, thold=0): """ Process attributes for unweighted (numeric) threshold thold Parameters: thold (int): non-negative threshold number M-of-N threshold """ if thold < 0: raise ValueError(f"Non-positive int threshold = {thold}.") self._thold = thold self._weighted = False self._size = self._thold # used to verify that keys list size is at least size self._satisfy = self._satisfy_numeric self._number = Number(num=thold) self._bexter = None def _processWeighted(self, thold=[]): """ Process attributes for fractionall weighted threshold thold Parameters: thold (iterable): iterable or iterable or iterables of rational number fraction strings >= 0 and <= 1 """ for clause in thold: # sum of top level weights in clause must be >= 1 # When element is dict then sum of value's weights must be >= 1 top = [] # top level weights for e in clause: if isinstance(e, tuple): top.append(e[0]) if not (sum(e[1]) >= 1): raise ValueError(f"Invalid sith clause = {clause}, " f"element = {e}. All nested clause " f"weight sums must be >= 1.") else: top.append(e) if not (sum(top) >= 1): raise ValueError(f"Invalid sith clause = {clause}, all top level" f"clause weight sums must be >= 1.") self._thold = thold self._weighted = True #self._size = sum(len(clause) for clause in thold) s = 0 for clause in thold: for e in clause: if isinstance(e, tuple): s += len(e[1]) else: s += 1 self._size = s self._satisfy = self._satisfy_weighted # make bext str of thold for .bexter for limen ta = [] # list of list of fractions and/or single element map of fractions for c in thold: bc = [] # list of fractions and/or single element map of fractions for e in c: if isinstance(e, tuple): f = e[0] k = f"{f.numerator}s{f.denominator}" if (0 < f < 1) else f"{int(f)}" v = "v".join([f"{f.numerator}s{f.denominator}" if (0 < f < 1) else f"{int(f)}" for f in e[1]]) kv = "k".join([k, v]) bc.append(kv) else: bc.append(f"{e.numerator}s{e.denominator}" if (0 < e < 1) else f"{int(e)}") ta.append(bc) bext = "a".join(["c".join(bc) for bc in ta]) self._number = None self._bexter = Bexter(bext=bext)
[docs] @staticmethod def weight(w: str) -> Fraction: """Returns valid weight from w else raises error (ValueError or TypeError). w expression must evaluate to 0, 1, or strict proper rational fraction. w expression must be 0 <= w <= 1 Else raises ValueError w must not be float else raises TypeError When not int w must be ratio of integers n/d else raise ValueError. Parameters: w (str): threshold weight expression """ try: # float str or ratio str raises ValueError if int(float(w)) != float(w): # float str raise TypeError("Invalid weight str got float w={w}.") w = int(w) # expression is int str except TypeError as ex: raise ValueError(str(ex)) from ex except ValueError as ex: # not float str or int str so try ration str w = Fraction(w) if not 0 <= w <= 1: raise ValueError(f"Invalid weight not 0 <= {w} <= 1.") return w
[docs] def satisfy(self, indices): """ Returns True if indices list of verified signature key indices satisfies threshold, False otherwise. Parameters: indices is list of non-negative indices (offsets into key list) of verified signatures. the indices may be in any order, they are normalized herein """ return (self._satisfy(indices=indices))
def _satisfy_numeric(self, indices): """ Returns True if satisfies numeric threshold False otherwise Parameters: indices is list of indices (offsets into key list) of verified signatures """ try: if self.thold > 0 and len(indices) >= self.thold: # at least one return True except Exception as ex: return False return False def _satisfy_weighted(self, indices): """ Returns True if satifies fractional weighted threshold False otherwise Parameters: indices is list of non-negative indices (offsets into key list) of verified signatures. the indices may be in any order, they are normalized herein """ try: if not indices: # empty indices return False # remove duplicates with set, sort low to high indices = sorted(set(indices)) sats = [False] * self.size # default all satifactions to False for idx in indices: sats[idx] = True # set verified signature index to True wio = 0 # weight index offset for clause in self.thold: cw = 0 # init clause weight for e in clause: if isinstance(e, tuple): vw = 0 # init element value weight for w in e[1]: # sum weights of value if sats[wio]: vw += w wio += 1 if vw >= 1: # element true cw += e[0] # add element key weight to clause weight else: w = e if sats[wio]: # verified signature so weight applies cw += w wio += 1 if cw < 1: # each clause must sum to at least 1 return False return True # all clauses have cw >= 1 including final one, AND true except Exception as ex: return False return False
[docs] class Dicter: """ Dicter class is base class for objects that can be stored in a Suber Dicter classes can be initialized by a dict and then expose bytes of JSON in the .raw property Subclasses can add semantically appropriate properties that extract / add specific keys to the underlying dict .pad """
[docs] def __init__(self, raw=b'', pad=None, dicter=None, label=Saids.i): """ Create Dicter from either raw bytes, pad dict, or other dicter instance Parameters: raw (bytes): raw JSON of dicter class pad (dict): data dict for class: dicter (Dicter): dicter instance to clone label (str): field name of the SAID field. """ self._label = label if raw: # deserialize raw using property setter self.raw = raw # raw property setter does the deserialization elif pad: # serialize ked using property setter self.pad = pad # pad property setter does the serialization elif dicter: self._clone(sad=dicter) else: raise ValueError("Improper initialization need sad, raw or ked.")
def _clone(self, sad): self._raw = sad.raw self._pad = sad.pad self._rid = sad.rid @property def raw(self): """ raw property getter """ return self._raw @raw.setter def raw(self, raw): """ raw property setter """ self._raw = raw self._pad = json.loads(self._raw.decode("utf-8")) if self._label not in self._pad or self._pad[self._label] == "": self._pad[self._label] = self._randomNonce() self._rid = self._pad[self._label] @property def pad(self): """ pad property getter""" return self._pad @pad.setter def pad(self, pad): """ pad property setter """ self._pad = pad if self._label not in self._pad or self._pad[self._label] == "": self._pad[self._label] = self._randomNonce() self._raw = json.dumps(self._pad).encode("utf-8") self._rid = self._pad[self._label] @property def rid(self): """ ID of dict data as str """ return self._rid
[docs] def pretty(self, *, size=1024): """ Returns str JSON of .ked with pretty formatting ToDo: add default size limit on pretty when used for syslog UDP MCU like 1024 for ogler.logger """ return json.dumps(self.pad, indent=1)[:size if size is not None else None]
@staticmethod def _randomNonce(): """ Generate a random 128 bits salt and encode as qb64 Returns: str: qb64 encoded 128 bits random salt Usually should use Salter().qb64 but this would create circular import """ preseed = pysodium.randombytes(pysodium.crypto_pwhash_SALTBYTES) seedqb64 = Matter(raw=preseed, code=MtrDex.Salt_128).qb64 return seedqb64