transcrypto 1.1.2__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
transcrypto/aes.py CHANGED
@@ -47,7 +47,7 @@ assert _PASSWORD_ITERATIONS == (6075308 + 1) // 3, 'should never happen: constan
47
47
 
48
48
 
49
49
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
50
- class AESKey(base.CryptoKey, base.SymmetricCrypto):
50
+ class AESKey(base.CryptoKey, base.Encryptor, base.Decryptor):
51
51
  """Advanced Encryption Standard (AES) 256 bits key (32 bytes).
52
52
 
53
53
  No measures are taken here to prevent timing attacks.
@@ -112,7 +112,7 @@ class AESKey(base.CryptoKey, base.SymmetricCrypto):
112
112
  salt=_PASSWORD_SALT_256, iterations=_PASSWORD_ITERATIONS)
113
113
  return cls(key256=kdf.derive(str_password.encode('utf-8')))
114
114
 
115
- class ECBEncoderClass(base.SymmetricCrypto):
115
+ class ECBEncoderClass(base.Encryptor, base.Decryptor):
116
116
  """The simplest encryption possible (UNSAFE if misused): 128 bit block AES-ECB, 256 bit key.
117
117
 
118
118
  Please DO **NOT** use this for regular cryptography. For regular crypto use Encrypt()/Decrypt().
@@ -245,7 +245,8 @@ class AESKey(base.CryptoKey, base.SymmetricCrypto):
245
245
  InputError: invalid inputs
246
246
  CryptoError: internal crypto failures, authentication failure, key mismatch, etc
247
247
  """
248
- assert len(ciphertext) >= 32, 'should never happen: AES256+GCM should have ≥32 bytes IV/CT/tag'
248
+ if len(ciphertext) < 32:
249
+ raise base.InputError(f'AES256+GCM should have ≥32 bytes IV/CT/tag: {len(ciphertext)}')
249
250
  iv, tag = ciphertext[:16], ciphertext[-16:]
250
251
  decryptor: ciphers.CipherContext = ciphers.Cipher(
251
252
  algorithms.AES256(self.key256), modes.GCM(iv, tag)).decryptor()
transcrypto/base.py CHANGED
@@ -19,12 +19,11 @@ import pickle
19
19
  # import pdb
20
20
  import secrets
21
21
  import time
22
- from typing import Any, Callable, final, MutableSequence, Self, TypeVar
23
-
22
+ from typing import Any, Callable, final, MutableSequence, Protocol, runtime_checkable, Self, TypeVar
24
23
  import zstandard
25
24
 
26
25
  __author__ = 'balparda@github.com'
27
- __version__ = '1.1.2' # v1.1.2, 2025-08-29
26
+ __version__ = '1.3.0' # 2025-09-07, Sun
28
27
  __version_tuple__: tuple[int, ...] = tuple(int(v) for v in __version__.split('.'))
29
28
 
30
29
  # MIN_TM = int( # minimum allowed timestamp
@@ -35,8 +34,8 @@ BytesToInt: Callable[[bytes], int] = lambda b: int.from_bytes(b, 'big', signed=F
35
34
  BytesToEncoded: Callable[[bytes], str] = lambda b: base64.urlsafe_b64encode(b).decode('ascii')
36
35
 
37
36
  HexToBytes: Callable[[str], bytes] = bytes.fromhex
38
- IntToBytes: Callable[[int], bytes] = lambda i: i.to_bytes(
39
- (i.bit_length() + 7) // 8, 'big', signed=False)
37
+ IntToFixedBytes: Callable[[int, int], bytes] = lambda i, n: i.to_bytes(n, 'big', signed=False)
38
+ IntToBytes: Callable[[int], bytes] = lambda i: IntToFixedBytes(i, (i.bit_length() + 7) // 8)
40
39
  IntToEncoded: Callable[[int], str] = lambda i: BytesToEncoded(IntToBytes(i))
41
40
  EncodedToBytes: Callable[[str], bytes] = lambda e: base64.urlsafe_b64decode(e.encode('ascii'))
42
41
 
@@ -46,7 +45,7 @@ PadBytesTo: Callable[[bytes, int], bytes] = lambda b, i: b.rjust((i + 7) // 8, b
46
45
  # these control the pickling of data, do NOT ever change, or you will break all databases
47
46
  # <https://docs.python.org/3/library/pickle.html#pickle.DEFAULT_PROTOCOL>
48
47
  _PICKLE_PROTOCOL = 4 # protocol 4 available since python v3.8 # do NOT ever change!
49
- _PICKLE_AAD = b'transcrypto.base.Serialize' # do NOT ever change!
48
+ _PICKLE_AAD = b'transcrypto.base.Serialize.1.0' # do NOT ever change!
50
49
  # these help find compressed files, do NOT change unless zstandard changes
51
50
  _ZSTD_MAGIC_FRAME = 0xFD2FB528
52
51
  _ZSTD_MAGIC_SKIPPABLE_MIN = 0x184D2A50
@@ -646,11 +645,11 @@ class CryptoKey(abc.ABC):
646
645
  return BytesToEncoded(self.blob)
647
646
 
648
647
  @final
649
- def Blob(self, /, *, key: SymmetricCrypto | None = None, silent: bool = True) -> bytes:
648
+ def Blob(self, /, *, key: Encryptor | None = None, silent: bool = True) -> bytes:
650
649
  """Serial (bytes) representation of the object with more options, including encryption.
651
650
 
652
651
  Args:
653
- key (SymmetricCrypto, optional): if given will key.Encrypt() data before saving
652
+ key (Encryptor, optional): if given will key.Encrypt() data before saving
654
653
  silent (bool, optional): if True (default) will not log
655
654
 
656
655
  Returns:
@@ -659,11 +658,11 @@ class CryptoKey(abc.ABC):
659
658
  return Serialize(self, compress=-2, key=key, silent=silent)
660
659
 
661
660
  @final
662
- def Encoded(self, /, *, key: SymmetricCrypto | None = None, silent: bool = True) -> str:
661
+ def Encoded(self, /, *, key: Encryptor | None = None, silent: bool = True) -> str:
663
662
  """Base-64 representation of the object with more options, including encryption.
664
663
 
665
664
  Args:
666
- key (SymmetricCrypto, optional): if given will key.Encrypt() data before saving
665
+ key (Encryptor, optional): if given will key.Encrypt() data before saving
667
666
  silent (bool, optional): if True (default) will not log
668
667
 
669
668
  Returns:
@@ -674,14 +673,13 @@ class CryptoKey(abc.ABC):
674
673
  @final
675
674
  @classmethod
676
675
  def Load(
677
- cls, data: str | bytes, /, *,
678
- key: SymmetricCrypto | None = None, silent: bool = True) -> Self:
676
+ cls, data: str | bytes, /, *, key: Decryptor | None = None, silent: bool = True) -> Self:
679
677
  """Load (create) object from serialized bytes or string.
680
678
 
681
679
  Args:
682
680
  data (str | bytes): if bytes is assumed from CryptoKey.blob/Blob(), and
683
681
  if string is assumed from CryptoKey.encoded/Encoded()
684
- key (SymmetricCrypto, optional): if given will key.Encrypt() data before saving
682
+ key (Decryptor, optional): if given will key.Encrypt() data before saving
685
683
  silent (bool, optional): if True (default) will not log
686
684
 
687
685
  Returns:
@@ -698,20 +696,21 @@ class CryptoKey(abc.ABC):
698
696
  return obj # type:ignore
699
697
 
700
698
 
701
- class SymmetricCrypto(abc.ABC):
702
- """Abstract interface for symmetric encryption.
699
+ @runtime_checkable
700
+ class Encryptor(Protocol): # pylint: disable=too-few-public-methods
701
+ """Abstract interface for a class that has encryption
703
702
 
704
703
  Contract:
705
704
  - If algorithm accepts a `nonce` or `tag` these have to be handled internally by the
706
- implementation and appended to the ciphertext.
705
+ implementation and appended to the `ciphertext`/`signature`.
707
706
  - If AEAD is supported, `associated_data` (AAD) must be authenticated. If not supported
708
707
  then `associated_data` different from None must raise InputError.
709
708
 
710
709
  Notes:
711
710
  The interface is deliberately minimal: byte-in / byte-out.
712
711
  Metadata like nonce/tag may be:
713
- - returned alongside ciphertext, or
714
- - bundled/serialized into `ciphertext` by the implementation.
712
+ - returned alongside `ciphertext`/`signature`, or
713
+ - bundled/serialized into `ciphertext`/`signature` by the implementation.
715
714
  """
716
715
 
717
716
  @abc.abstractmethod
@@ -732,6 +731,11 @@ class SymmetricCrypto(abc.ABC):
732
731
  CryptoError: internal crypto failures
733
732
  """
734
733
 
734
+
735
+ @runtime_checkable
736
+ class Decryptor(Protocol): # pylint: disable=too-few-public-methods
737
+ """Abstract interface for a class that has decryption (see contract/notes in Encryptor)."""
738
+
735
739
  @abc.abstractmethod
736
740
  def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
737
741
  """Decrypt `ciphertext` and return the original `plaintext`.
@@ -749,9 +753,55 @@ class SymmetricCrypto(abc.ABC):
749
753
  """
750
754
 
751
755
 
756
+ @runtime_checkable
757
+ class Verifier(Protocol): # pylint: disable=too-few-public-methods
758
+ """Abstract interface for asymmetric signature verify. (see contract/notes in Encryptor)."""
759
+
760
+ @abc.abstractmethod
761
+ def Verify(
762
+ self, message: bytes, signature: bytes, /, *, associated_data: bytes | None = None) -> bool:
763
+ """Verify a `signature` for `message`. True if OK; False if failed verification.
764
+
765
+ Args:
766
+ message (bytes): Data that was signed (including any embedded nonce/tag if applicable)
767
+ signature (bytes): Signature data to verify (including any embedded nonce/tag if applicable)
768
+ associated_data (bytes, optional): Optional AAD (must match what was used during signing)
769
+
770
+ Returns:
771
+ True if signature is valid, False otherwise
772
+
773
+ Raises:
774
+ InputError: invalid inputs
775
+ CryptoError: internal crypto failures, authentication failure, key mismatch, etc
776
+ """
777
+
778
+
779
+ @runtime_checkable
780
+ class Signer(Protocol): # pylint: disable=too-few-public-methods
781
+ """Abstract interface for asymmetric signing. (see contract/notes in Encryptor)."""
782
+
783
+ @abc.abstractmethod
784
+ def Sign(self, message: bytes, /, *, associated_data: bytes | None = None) -> bytes:
785
+ """Sign `message` and return the `signature`.
786
+
787
+ Args:
788
+ message (bytes): Data to sign.
789
+ associated_data (bytes, optional): Optional AAD for AEAD modes; must be
790
+ provided again on decrypt
791
+
792
+ Returns:
793
+ bytes: Signature; if a nonce/tag is needed for decryption, the implementation
794
+ must encode it within the returned bytes (or document how to retrieve it)
795
+
796
+ Raises:
797
+ InputError: invalid inputs
798
+ CryptoError: internal crypto failures
799
+ """
800
+
801
+
752
802
  def Serialize(
753
803
  python_obj: Any, /, *, file_path: str | None = None,
754
- compress: int | None = 3, key: SymmetricCrypto | None = None, silent: bool = False) -> bytes:
804
+ compress: int | None = 3, key: Encryptor | None = None, silent: bool = False) -> bytes:
755
805
  """Serialize a Python object into a BLOB, optionally compress / encrypt / save to disk.
756
806
 
757
807
  Data path is:
@@ -777,7 +827,7 @@ def Serialize(
777
827
  file_path (str, optional): full path to optionally save the data to
778
828
  compress (int | None, optional): Compress level before encrypting/saving; -22 ≤ compress ≤ 22;
779
829
  None is no compression; default is 3, which is fast, see table above for other values
780
- key (SymmetricCrypto, optional): if given will key.Encrypt() data before saving
830
+ key (Encryptor, optional): if given will key.Encrypt() data before saving
781
831
  silent (bool, optional): if True will not log; default is False (will log)
782
832
 
783
833
  Returns:
@@ -819,7 +869,7 @@ def Serialize(
819
869
 
820
870
  def DeSerialize(
821
871
  *, data: bytes | None = None, file_path: str | None = None,
822
- key: SymmetricCrypto | None = None, silent: bool = False) -> Any:
872
+ key: Decryptor | None = None, silent: bool = False) -> Any:
823
873
  """Loads (de-serializes) a BLOB back to a Python object, optionally decrypting / decompressing.
824
874
 
825
875
  Data path is:
@@ -835,7 +885,7 @@ def DeSerialize(
835
885
  if you use this option, `file_path` will be ignored
836
886
  file_path (str, optional): if given, use this as file path to load binary data string (input);
837
887
  if you use this option, `data` will be ignored
838
- key (SymmetricCrypto, optional): if given will key.Decrypt() data before decompressing/loading
888
+ key (Decryptor, optional): if given will key.Decrypt() data before decompressing/loading
839
889
  silent (bool, optional): if True will not log; default is False (will log)
840
890
 
841
891
  Returns:
@@ -893,7 +943,7 @@ def DeSerialize(
893
943
 
894
944
 
895
945
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
896
- class PublicBid(CryptoKey):
946
+ class PublicBid512(CryptoKey):
897
947
  """Public commitment to a (cryptographically secure) bid that can be revealed/validated later.
898
948
 
899
949
  Bid is computed as: public_hash = Hash512(public_key || private_key || secret_bid)
@@ -901,6 +951,8 @@ class PublicBid(CryptoKey):
901
951
  Everything is bytes. The public part is (public_key, public_hash) and the private
902
952
  part is (private_key, secret_bid). The whole computation can be checked later.
903
953
 
954
+ No measures are taken here to prevent timing attacks (probably not a concern).
955
+
904
956
  Attributes:
905
957
  public_key (bytes): 512-bits random value
906
958
  public_hash (bytes): SHA-512 hash of (public_key || private_key || secret_bid)
@@ -915,7 +967,7 @@ class PublicBid(CryptoKey):
915
967
  Raises:
916
968
  InputError: invalid inputs
917
969
  """
918
- super(PublicBid, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
970
+ super(PublicBid512, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
919
971
  if len(self.public_key) != 64 or len(self.public_hash) != 64:
920
972
  raise InputError(f'invalid public_key or public_hash: {self}')
921
973
 
@@ -925,7 +977,8 @@ class PublicBid(CryptoKey):
925
977
  Returns:
926
978
  string representation of PublicBid
927
979
  """
928
- return (f'PublicBid(public_key={BytesToEncoded(self.public_key)}, '
980
+ return ('PublicBid512('
981
+ f'public_key={BytesToEncoded(self.public_key)}, '
929
982
  f'public_hash={BytesToHex(self.public_hash)})')
930
983
 
931
984
  def VerifyBid(self, private_key: bytes, secret: bytes, /) -> bool:
@@ -943,7 +996,7 @@ class PublicBid(CryptoKey):
943
996
  """
944
997
  try:
945
998
  # creating the PrivateBid object will validate everything; InputError we allow to propagate
946
- PrivateBid(
999
+ PrivateBid512(
947
1000
  public_key=self.public_key, public_hash=self.public_hash,
948
1001
  private_key=private_key, secret_bid=secret)
949
1002
  return True # if we got here, all is good
@@ -951,13 +1004,13 @@ class PublicBid(CryptoKey):
951
1004
  return False # bid does not match the public commitment
952
1005
 
953
1006
  @classmethod
954
- def Copy(cls, other: PublicBid, /) -> Self:
1007
+ def Copy(cls, other: PublicBid512, /) -> Self:
955
1008
  """Initialize a public bid by taking the public parts of a public/private bid."""
956
1009
  return cls(public_key=other.public_key, public_hash=other.public_hash)
957
1010
 
958
1011
 
959
1012
  @dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
960
- class PrivateBid(PublicBid):
1013
+ class PrivateBid512(PublicBid512):
961
1014
  """Private bid that can be revealed and validated against a public commitment (see PublicBid).
962
1015
 
963
1016
  Attributes:
@@ -975,7 +1028,7 @@ class PrivateBid(PublicBid):
975
1028
  InputError: invalid inputs
976
1029
  CryptoError: bid does not match the public commitment
977
1030
  """
978
- super(PrivateBid, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
1031
+ super(PrivateBid512, self).__post_init__() # pylint: disable=super-with-arguments # needed here b/c: dataclass
979
1032
  if len(self.private_key) != 64 or len(self.secret_bid) < 1:
980
1033
  raise InputError(f'invalid private_key or secret_bid: {self}')
981
1034
  if self.public_hash != Hash512(self.public_key + self.private_key + self.secret_bid):
@@ -987,7 +1040,8 @@ class PrivateBid(PublicBid):
987
1040
  Returns:
988
1041
  string representation of PrivateBid without leaking secrets
989
1042
  """
990
- return (f'PrivateBid({super(PrivateBid, self).__str__()}, ' # pylint: disable=super-with-arguments
1043
+ return ('PrivateBid512('
1044
+ f'{super(PrivateBid512, self).__str__()}, ' # pylint: disable=super-with-arguments
991
1045
  f'private_key={ObfuscateSecret(self.private_key)}, '
992
1046
  f'secret_bid={ObfuscateSecret(self.secret_bid)})')
993
1047