transcrypto 1.1.2__py3-none-any.whl → 1.2.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 +4 -3
- transcrypto/base.py +84 -30
- transcrypto/dsa.py +153 -19
- transcrypto/elgamal.py +224 -28
- transcrypto/rsa.py +203 -19
- transcrypto/sss.py +159 -20
- transcrypto/transcrypto.py +401 -178
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/METADATA +683 -425
- transcrypto-1.2.0.dist-info/RECORD +15 -0
- transcrypto-1.1.2.dist-info/RECORD +0 -15
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/top_level.txt +0 -0
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
26
|
+
__version__ = '1.2.0' # 2025-09-05, Fri
|
|
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
|
-
|
|
39
|
-
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
702
|
-
|
|
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
|
|
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:
|
|
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 (
|
|
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:
|
|
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 (
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
|
transcrypto/dsa.py
CHANGED
|
@@ -17,21 +17,30 @@ import logging
|
|
|
17
17
|
# import pdb
|
|
18
18
|
from typing import Self
|
|
19
19
|
|
|
20
|
-
from . import base
|
|
21
|
-
from . import modmath
|
|
20
|
+
from . import base, modmath
|
|
22
21
|
|
|
23
22
|
__author__ = 'balparda@github.com'
|
|
24
23
|
__version__: str = base.__version__ # version comes from base!
|
|
25
24
|
__version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
_PRIME_MULTIPLE_SEARCH =
|
|
27
|
+
_PRIME_MULTIPLE_SEARCH = 4096 # how many multiples of q to try before restarting
|
|
29
28
|
_MAX_KEY_GENERATION_FAILURES = 15
|
|
30
29
|
|
|
30
|
+
# fixed prefixes: do NOT ever change! will break all encryption and signature schemes
|
|
31
|
+
_DSA_SIGNATURE_HASH_PREFIX = b'transcrypto.DSA.Signature.1.0\x00'
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
33
35
|
"""Generates 2 random DSA primes p & q with `x_bits` size and (p-1)%q==0.
|
|
34
36
|
|
|
37
|
+
Uses an aggressive small-prime wheel sieve:
|
|
38
|
+
Before any Miller-Rabin we reject p = m·q + 1 if it is divisible by a small prime.
|
|
39
|
+
We precompute forbidden residues for m:
|
|
40
|
+
• For each small prime r (all primes up to, say, 100 000), we compute
|
|
41
|
+
m_forbidden ≡ -q⁻¹ (mod r) (because (m·q + 1) % r == 0 ⇔ m ≡ -q⁻¹ (mod r))
|
|
42
|
+
• When we iterate m, we skip values that hit any forbidden residue class.
|
|
43
|
+
|
|
35
44
|
Args:
|
|
36
45
|
p_bits (int): Number of guaranteed bits in `p` prime representation,
|
|
37
46
|
p_bits ≥ q_bits + 11
|
|
@@ -50,23 +59,45 @@ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
|
50
59
|
if p_bits < q_bits + 11:
|
|
51
60
|
raise base.InputError(f'invalid p_bits length: {p_bits=}')
|
|
52
61
|
# make q
|
|
53
|
-
q = modmath.NBitRandomPrime(q_bits)
|
|
62
|
+
q: int = modmath.NBitRandomPrime(q_bits)
|
|
63
|
+
# make list of small primes to use for sieving
|
|
64
|
+
approx_q_root: int = 1 << (q_bits // 2)
|
|
65
|
+
forbidden: dict[int, int] = {} # (modulus: forbidden residue)
|
|
66
|
+
for r in modmath.PrimeGenerator(3):
|
|
67
|
+
forbidden[r] = (-modmath.ModInv(q % r, r)) % r
|
|
68
|
+
if r > 100000 or r > approx_q_root:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
def _PassesSieve(m: int) -> bool:
|
|
72
|
+
for r, f in forbidden.items():
|
|
73
|
+
if m % r == f:
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
54
77
|
# find range of multiples to use
|
|
55
78
|
min_p, max_p = 2 ** (p_bits - 1), 2 ** p_bits - 1
|
|
56
79
|
min_m, max_m = min_p // q + 2, max_p // q - 2
|
|
57
80
|
assert max_m - min_m > 1000 # make sure we'll have options!
|
|
58
81
|
# start searching from a random multiple
|
|
59
82
|
failures: int = 0
|
|
83
|
+
window: int = max(_PRIME_MULTIPLE_SEARCH, 2 * p_bits)
|
|
60
84
|
while True:
|
|
61
85
|
# try searching starting here
|
|
62
86
|
m: int = base.RandInt(min_m, max_m)
|
|
63
|
-
|
|
87
|
+
if m % 2:
|
|
88
|
+
m += 1 # make even
|
|
89
|
+
for _ in range(window):
|
|
64
90
|
p: int = q * m + 1
|
|
65
91
|
if p >= max_p:
|
|
66
92
|
break
|
|
93
|
+
# first do a quick sieve test
|
|
94
|
+
if not _PassesSieve(m):
|
|
95
|
+
m += 2
|
|
96
|
+
continue
|
|
97
|
+
# passed sieve, do full test
|
|
67
98
|
if modmath.IsPrime(p):
|
|
68
99
|
return (p, q, m) # found a suitable prime set!
|
|
69
|
-
m +=
|
|
100
|
+
m += 2 # next multiple
|
|
70
101
|
# after _PRIME_MULTIPLE_SEARCH we declare this range failed
|
|
71
102
|
failures += 1
|
|
72
103
|
if failures >= _MAX_KEY_GENERATION_FAILURES:
|
|
@@ -78,8 +109,6 @@ def NBitRandomDSAPrimes(p_bits: int, q_bits: int, /) -> tuple[int, int, int]:
|
|
|
78
109
|
class DSASharedPublicKey(base.CryptoKey):
|
|
79
110
|
"""DSA shared public key. This key can be shared by a group.
|
|
80
111
|
|
|
81
|
-
BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
|
|
82
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
83
112
|
No measures are taken here to prevent timing attacks.
|
|
84
113
|
|
|
85
114
|
Attributes:
|
|
@@ -116,10 +145,43 @@ class DSASharedPublicKey(base.CryptoKey):
|
|
|
116
145
|
string representation of DSASharedPublicKey
|
|
117
146
|
"""
|
|
118
147
|
return ('DSASharedPublicKey('
|
|
148
|
+
f'bits=[{self.prime_modulus.bit_length()}, {self.prime_seed.bit_length()}], '
|
|
119
149
|
f'prime_modulus={base.IntToEncoded(self.prime_modulus)}, '
|
|
120
150
|
f'prime_seed={base.IntToEncoded(self.prime_seed)}, '
|
|
121
151
|
f'group_base={base.IntToEncoded(self.group_base)})')
|
|
122
152
|
|
|
153
|
+
@property
|
|
154
|
+
def modulus_size(self) -> tuple[int, int]:
|
|
155
|
+
"""Modulus size in bytes. The number of bytes used in Sign/Verify."""
|
|
156
|
+
return ((self.prime_modulus.bit_length() + 7) // 8,
|
|
157
|
+
(self.prime_seed.bit_length() + 7) // 8)
|
|
158
|
+
|
|
159
|
+
def _DomainSeparatedHash(
|
|
160
|
+
self, message: bytes, associated_data: bytes | None, salt: bytes, /) -> int:
|
|
161
|
+
"""Compute the domain-separated hash for signing and verifying.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
message (bytes): message to sign/verify
|
|
165
|
+
associated_data (bytes | None): optional associated data
|
|
166
|
+
salt (bytes): salt to use in the hash
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
int: integer representation of the hash output;
|
|
170
|
+
Hash512("prefix" || len(aad) || aad || message || salt)
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
CryptoError: hash output is out of range
|
|
174
|
+
"""
|
|
175
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
176
|
+
la: bytes = base.IntToFixedBytes(len(aad), 8)
|
|
177
|
+
assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes'
|
|
178
|
+
y: int = base.BytesToInt(
|
|
179
|
+
base.Hash512(_DSA_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
|
|
180
|
+
if not 1 < y < self.prime_seed - 1:
|
|
181
|
+
# will only reasonably happen if prime seed is small
|
|
182
|
+
raise base.CryptoError(f'hash output {y} is out of range/invalid {self.prime_seed}')
|
|
183
|
+
return y
|
|
184
|
+
|
|
123
185
|
@classmethod
|
|
124
186
|
def NewShared(cls, p_bits: int, q_bits: int, /) -> Self:
|
|
125
187
|
"""Make a new shared public key of `bit_length` bits.
|
|
@@ -146,11 +208,9 @@ class DSASharedPublicKey(base.CryptoKey):
|
|
|
146
208
|
|
|
147
209
|
|
|
148
210
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
149
|
-
class DSAPublicKey(DSASharedPublicKey):
|
|
211
|
+
class DSAPublicKey(DSASharedPublicKey, base.Verifier):
|
|
150
212
|
"""DSA public key. This is an individual public key.
|
|
151
213
|
|
|
152
|
-
BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
|
|
153
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
154
214
|
No measures are taken here to prevent timing attacks.
|
|
155
215
|
|
|
156
216
|
Attributes:
|
|
@@ -176,11 +236,12 @@ class DSAPublicKey(DSASharedPublicKey):
|
|
|
176
236
|
Returns:
|
|
177
237
|
string representation of DSAPublicKey
|
|
178
238
|
"""
|
|
179
|
-
return (
|
|
239
|
+
return ('DSAPublicKey('
|
|
240
|
+
f'{super(DSAPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
180
241
|
f'individual_base={base.IntToEncoded(self.individual_base)})')
|
|
181
242
|
|
|
182
243
|
def _MakeEphemeralKey(self) -> tuple[int, int]:
|
|
183
|
-
"""Make an ephemeral key adequate to be used with
|
|
244
|
+
"""Make an ephemeral key adequate to be used with DSA.
|
|
184
245
|
|
|
185
246
|
Returns:
|
|
186
247
|
(key, key_inverse), where 3 ≤ k < p_seed and (k*i) % p_seed == 1
|
|
@@ -192,9 +253,11 @@ class DSAPublicKey(DSASharedPublicKey):
|
|
|
192
253
|
ephemeral_key = base.RandBits(bit_length - 1)
|
|
193
254
|
return (ephemeral_key, modmath.ModInv(ephemeral_key, self.prime_seed))
|
|
194
255
|
|
|
195
|
-
def
|
|
256
|
+
def RawVerify(self, message: int, signature: tuple[int, int], /) -> bool:
|
|
196
257
|
"""Verify a signature. True if OK; False if failed verification.
|
|
197
258
|
|
|
259
|
+
BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
|
|
260
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
198
261
|
We explicitly disallow `message` to be zero.
|
|
199
262
|
|
|
200
263
|
Args:
|
|
@@ -221,6 +284,42 @@ class DSAPublicKey(DSASharedPublicKey):
|
|
|
221
284
|
self.individual_base, (signature[0] * inv) % self.prime_seed, self.prime_modulus)
|
|
222
285
|
return ((a * b) % self.prime_modulus) % self.prime_seed == signature[0]
|
|
223
286
|
|
|
287
|
+
def Verify(
|
|
288
|
+
self, message: bytes, signature: bytes, /, *, associated_data: bytes | None = None) -> bool:
|
|
289
|
+
"""Verify a `signature` for `message`. True if OK; False if failed verification.
|
|
290
|
+
|
|
291
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
292
|
+
• Split signature in 3 parts: the first 64 bytes is salt, the rest is s1 and s2
|
|
293
|
+
• y_check = DSA(s1, s2)
|
|
294
|
+
• return y_check == Hash512("prefix" || len(aad) || aad || message || salt)
|
|
295
|
+
• return False for any malformed signature
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
message (bytes): Data that was signed
|
|
299
|
+
signature (bytes): Signature data to verify
|
|
300
|
+
associated_data (bytes, optional): Optional AAD (must match what was used during signing)
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
True if signature is valid, False otherwise
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
InputError: invalid inputs
|
|
307
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
308
|
+
"""
|
|
309
|
+
k: int = self.modulus_size[1] # use prime_seed size
|
|
310
|
+
if k <= 64:
|
|
311
|
+
raise base.InputError(f'modulus/seed too small for signing operations: {k} bytes')
|
|
312
|
+
if len(signature) != (64 + k + k):
|
|
313
|
+
logging.info(f'invalid signature length: {len(signature)} ; expected {64 + k + k}')
|
|
314
|
+
return False
|
|
315
|
+
try:
|
|
316
|
+
return self.RawVerify(
|
|
317
|
+
self._DomainSeparatedHash(message, associated_data, signature[:64]),
|
|
318
|
+
(base.BytesToInt(signature[64:64 + k]), base.BytesToInt(signature[64 + k:])))
|
|
319
|
+
except base.InputError as err:
|
|
320
|
+
logging.info(err)
|
|
321
|
+
return False
|
|
322
|
+
|
|
224
323
|
@classmethod
|
|
225
324
|
def Copy(cls, other: DSAPublicKey, /) -> Self:
|
|
226
325
|
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
@@ -232,11 +331,9 @@ class DSAPublicKey(DSASharedPublicKey):
|
|
|
232
331
|
|
|
233
332
|
|
|
234
333
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
235
|
-
class DSAPrivateKey(DSAPublicKey):
|
|
334
|
+
class DSAPrivateKey(DSAPublicKey, base.Signer): # pylint: disable=too-many-ancestors
|
|
236
335
|
"""DSA private key.
|
|
237
336
|
|
|
238
|
-
BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
|
|
239
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
240
337
|
No measures are taken here to prevent timing attacks.
|
|
241
338
|
|
|
242
339
|
Attributes:
|
|
@@ -266,12 +363,15 @@ class DSAPrivateKey(DSAPublicKey):
|
|
|
266
363
|
Returns:
|
|
267
364
|
string representation of DSAPrivateKey without leaking secrets
|
|
268
365
|
"""
|
|
269
|
-
return (
|
|
366
|
+
return ('DSAPrivateKey('
|
|
367
|
+
f'{super(DSAPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
270
368
|
f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
|
|
271
369
|
|
|
272
|
-
def
|
|
370
|
+
def RawSign(self, message: int, /) -> tuple[int, int]:
|
|
273
371
|
"""Sign `message` with this private key.
|
|
274
372
|
|
|
373
|
+
BEWARE: This is raw DSA, no ECDSA/EdDSA padding, no hash, no validation!
|
|
374
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
275
375
|
We explicitly disallow `message` to be zero.
|
|
276
376
|
|
|
277
377
|
Args:
|
|
@@ -294,6 +394,40 @@ class DSAPrivateKey(DSAPublicKey):
|
|
|
294
394
|
b = (ephemeral_inv * ((message + a * self.decrypt_exp) % self.prime_seed)) % self.prime_seed
|
|
295
395
|
return (a, b)
|
|
296
396
|
|
|
397
|
+
def Sign(self, message: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
398
|
+
"""Sign `message` and return the `signature`.
|
|
399
|
+
|
|
400
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
401
|
+
• Pick random salt of 64 bytes
|
|
402
|
+
• s1, s2 = DSA(Hash512("prefix" || len(aad) || aad || message || salt))
|
|
403
|
+
• return salt || Padded(s1, k) || Padded(s2, k)
|
|
404
|
+
|
|
405
|
+
This is basically Full-Domain Hash DSA with a 512-bit hash and per-signature salt,
|
|
406
|
+
which is EUF-CMA secure in the ROM. Our domain-separation prefix and explicit AAD
|
|
407
|
+
length prefix are both correct and remove composition/ambiguity pitfalls.
|
|
408
|
+
There are no Bleichenbacher-style issue because we do not expose any padding semantics.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
message (bytes): Data to sign.
|
|
412
|
+
associated_data (bytes, optional): Optional AAD for AEAD modes; must be
|
|
413
|
+
provided again on decrypt
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
bytes: Signature; salt || Padded(s, k) - see above
|
|
417
|
+
|
|
418
|
+
Raises:
|
|
419
|
+
InputError: invalid inputs
|
|
420
|
+
CryptoError: internal crypto failures
|
|
421
|
+
"""
|
|
422
|
+
k: int = self.modulus_size[1] # use prime_seed size
|
|
423
|
+
if k <= 64:
|
|
424
|
+
raise base.InputError(f'modulus/seed too small for signing operations: {k} bytes')
|
|
425
|
+
salt: bytes = base.RandBytes(64)
|
|
426
|
+
s_int: tuple[int, int] = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
|
|
427
|
+
s_bytes: bytes = base.IntToFixedBytes(s_int[0], k) + base.IntToFixedBytes(s_int[1], k)
|
|
428
|
+
assert len(s_bytes) == 2 * k, 'should never happen: s_bytes should be exactly 2k bytes'
|
|
429
|
+
return salt + s_bytes
|
|
430
|
+
|
|
297
431
|
@classmethod
|
|
298
432
|
def New(cls, shared_key: DSASharedPublicKey, /) -> Self:
|
|
299
433
|
"""Make a new private key based on an existing shared public key.
|