From 2918f86cb465f7f4d3d08252dce69c758bccd273 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 3 Aug 2024 13:07:52 -0600 Subject: [PATCH 01/17] fix doc strings comments --- src/keri/app/habbing.py | 6 +++--- src/keri/app/keeping.py | 7 +++--- src/keri/core/streaming.py | 44 ++++++++++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/keri/app/habbing.py b/src/keri/app/habbing.py index 44516caa8..d32a858f5 100644 --- a/src/keri/app/habbing.py +++ b/src/keri/app/habbing.py @@ -1321,13 +1321,13 @@ def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kw ondices=ondices) def decrypt(self, ser, verfers=None, **kwa): - """Sign given serialization ser using appropriate keys. + """Decrypt given serialization ser using appropriate keys. Use provided verfers or .kever.verfers to lookup keys to sign. Parameters: - ser (bytes): serialization to sign + ser (bytes): serialization to decrypt verfers (list[Verfer] | None): Verfer instances to get pub verifier - keys to lookup private siging keys. + keys to lookup and convert to private decryption keys. verfers None means use .kever.verfers. Assumes that when group and verfers is not None then provided verfers must be .kever.verfers diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 3a626a622..0e29fff6f 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -1394,10 +1394,10 @@ def sign(self, ser, pubs=None, verfers=None, indexed=True, cigars.append(signer.sign(ser)) # assigns .verfer to cigar return cigars + def decrypt(self, ser, pubs=None, verfers=None): """ - Returns list of signatures of ser if indexed as Sigers else as Cigars with - .verfer assigned. + Returns plain text of decrypted serialization Parameters: ser (bytes): serialization to sign @@ -1405,7 +1405,8 @@ def decrypt(self, ser, pubs=None, verfers=None): one of pubs or verfers is required. If both then verfers is ignored. verfers (list[Verfer] | None): Verfer instances of public keys one of pubs or verfers is required. If both then verfers is ignored. - If not pubs then gets public key from verfer.qb64 + If not pubs then gets public key from verfer.qb64 used to lookup + private keys Returns: bytes: decrypted data diff --git a/src/keri/core/streaming.py b/src/keri/core/streaming.py index 617a74d21..4ad253ecc 100644 --- a/src/keri/core/streaming.py +++ b/src/keri/core/streaming.py @@ -48,7 +48,7 @@ def annot(ims): if not isinstance(ims, bytearray): # going to strip - ims = bytearray(ims) # so make bytearray copy + ims = bytearray(ims) # so make bytearray copy, converts str to bytearray while ims: # right now just for KERI event messages cold = sniff(ims) # check for spurious counters at front of stream @@ -342,14 +342,22 @@ class Streamer: def __init__(self, stream): """Initialize instance + Holds sniffable CESR stream as byte like string + either (bytes, bytearray, or memoryview) Parameters: - stream (bytes | bytearray): sniffable CESR stream + stream (str | bytes | bytearray | memoryview): sniffable CESR stream + """ - self._stream = bytes(stream) + if hasattr(stream, "encode"): + stream = bytearray(stream.encode()) # convert str to bytearray + if not isinstance(stream, (bytes, bytearray, memoryview)): + raise kering.InvalidTypeError(f"Invalid stream type, not byteable.") + + self._stream = stream @property @@ -360,7 +368,9 @@ def stream(self): @property def text(self): - """expanded stream as qb64 text + """expanded stream where all primitives and groups in stream are + individually expanded to qb64. + Requires parsing full depth to ensure expanded consistently. Returns: stream (bytes): expanded text qb64 version of stream @@ -369,7 +379,9 @@ def text(self): @property def binary(self): - """compacted stream as qb2 binary + """compacted stream where all primitives and groups in stream are + individually compacted to qb2. + Requires parsing full depth to ensure compacted consistently Returns: stream (bytes): compacted binary qb2 version of stream @@ -378,13 +390,33 @@ def binary(self): @property def texter(self): - """expanded stream as Texter instance + """stream as Texter instance. + Texter(text=self.stream) Returns: texter (Texter): Texter primitive of stream suitable wrapping """ return self._stream + @property + def bexter(self): + """stream as Bexter instance. + Bexter of expanded text version of stream. + First expand to text which requires parsing then create bexter + Bexter(bext=self.text) + Because sniffable stream MUST NOT start with 'A' then there is no + length ambiguity. The only tritet collison of 'A' is with '-' but the + remaining 5 bits are guaranteed to always be different. So bexter must + check not just the starting tritet but the full starting byte to ensure + not 'A' as first byte. + + Requires parsing to ensure qb64 + Returns: + bexter (Bexter): Bexter primitive of stream suitable wrapping + + """ + return self._stream + From edbdcee4809eb0313cc5c9e0044fd7471dcbafe4 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 3 Aug 2024 13:37:59 -0600 Subject: [PATCH 02/17] start refactoring Encrypter Fix comments doc strings --- src/keri/app/keeping.py | 2 +- src/keri/core/signing.py | 23 +++++++++++++---------- src/keri/db/subing.py | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 0e29fff6f..6949d748d 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -805,7 +805,7 @@ def updateAeid(self, aeid, seed): for keys, data in self.ks.prms.getItemIter(): # keys is tuple of pre qb64 if data.salt: salter = self.decrypter.decrypt(ser=data.salt) - data.salt = (self.encrypter.encrypt(matter=salter).qb64 + data.salt = (self.encrypter.encrypt(prim=salter).qb64 if self.encrypter else salter.qb64) self.ks.prms.pin(keys, val=data) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index dcfaec05a..e448bcbc0 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -794,33 +794,36 @@ def verifySeed(self, seed): pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey) return (pubkey == self.raw) - def encrypt(self, ser=None, matter=None): + def encrypt(self, ser=None, prim=None, code=None): """ Returns: Cipher instance of cipher text encryption of plain text serialization provided by either ser or Matter instance when provided. Parameters: + ser (Union[bytes,str]): qb64b or qb64 serialization of plain text - matter (Matter): plain text as Matter instance of seed or salt to - be encrypted + prim (Matter | Indexer): CESR primitive instance whose serialization + qb64 or qb2 is to be encrypted based on code + code (str): code of plain text type for resultant encrypted cipher """ - if not (ser or matter): - raise EmptyMaterialError("Neither ser or plain are provided.") + if not (ser or prim): + raise EmptyMaterialError(f"Neither bar serialization or primitive " + f"are provided.") if ser: - matter = Matter(qb64b=ser) + prim = Matter(qb64b=ser) - if matter.code == MtrDex.Salt_128: # future other salt codes + if prim.code == MtrDex.Salt_128: # future other salt codes code = MtrDex.X25519_Cipher_Salt - elif matter.code == MtrDex.Ed25519_Seed: # future other seed codes + elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes code = MtrDex.X25519_Cipher_Seed else: - raise ValueError("Unsupported plain text code = {}.".format(matter.code)) + raise ValueError("Unsupported plain text code = {}.".format(prim.code)) # encrypting fully qualified qb64 version of plain text ensures its # derivation code round trips through eventual decryption - return (self._encrypt(ser=matter.qb64b, pubkey=self.raw, code=code)) + return (self._encrypt(ser=prim.qb64b, pubkey=self.raw, code=code)) @staticmethod def _x25519(ser, pubkey, code): diff --git a/src/keri/db/subing.py b/src/keri/db/subing.py index f007a28aa..5d092b53c 100644 --- a/src/keri/db/subing.py +++ b/src/keri/db/subing.py @@ -949,7 +949,7 @@ def put(self, keys: Union[str, Iterable], val: coring.Matter, already in database. """ if encrypter: - val = encrypter.encrypt(matter=val) # returns Cipher instance + val = encrypter.encrypt(prim=val) # returns Cipher instance return (self.db.putVal(db=self.sdb, key=self._tokey(keys), val=val.qb64b)) @@ -970,7 +970,7 @@ def pin(self, keys: Union[str, Iterable], val: coring.Matter, result (bool): True If successful. False otherwise. """ if encrypter: - val = encrypter.encrypt(matter=val) # returns Cipher instance + val = encrypter.encrypt(prim=val) # returns Cipher instance return (self.db.setVal(db=self.sdb, key=self._tokey(keys), val=val.qb64b)) From 108a27434762fa29979da25aba4005c4a241b38e Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 3 Aug 2024 14:13:20 -0600 Subject: [PATCH 03/17] Refactored Encrypter to support all cipher codes. Need units tests --- src/keri/app/keeping.py | 8 +++-- src/keri/core/signing.py | 65 ++++++++++++++++++++++++++++---------- tests/core/test_signing.py | 8 ++--- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 6949d748d..b1d2f1089 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -902,7 +902,7 @@ def salt(self, salt): may be plain text or cipher text handled by updateAeid """ if self.encrypter: - salt = self.encrypter.encrypt(ser=salt).qb64 + salt = self.encrypter.encrypt(ser=salt, code=core.MtrDex.X25519_Cipher_Salt).qb64 self.ks.gbls.pin('salt', salt) @@ -1020,7 +1020,8 @@ def incept(self, icodes=None, icount=1, icode=coring.MtrDex.Ed25519_Seed, if creator.salt: pp.salt = (creator.salt if not self.encrypter - else self.encrypter.encrypt(ser=creator.salt).qb64) + else self.encrypter.encrypt(ser=creator.salt, + code=core.MtrDex.X25519_Cipher_Salt).qb64) dt = helping.nowIso8601() ps = PreSit( @@ -1543,7 +1544,8 @@ def ingest(self, secrecies, iridx=0, ncount=1, ncode=coring.MtrDex.Ed25519_Seed, pp = PrePrm(pidx=pidx, algo=algo, salt=(creator.salt if not self.encrypter - else self.encrypter.encrypt(ser=creator.salt).qb64), + else self.encrypter.encrypt(ser=creator.salt, + code=core.MtrDex.X25519_Cipher_Salt).qb64), stem=creator.stem, tier=creator.tier) pre = csigners[0].verfer.qb64b diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index e448bcbc0..3df201ec2 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -12,7 +12,8 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from cryptography.hazmat.primitives.asymmetric import ec, utils -from ..kering import (EmptyMaterialError, InvalidCodeError, InvalidSizeError) +from ..kering import (EmptyMaterialError, InvalidCodeError, InvalidSizeError, + InvalidValueError) from ..help import helping @@ -766,8 +767,8 @@ def __init__(self, raw=None, code=MtrDex.X25519, verkey=None, **kwa): if not raw and verkey: verfer = Verfer(qb64b=verkey) if verfer.code not in (MtrDex.Ed25519N, MtrDex.Ed25519): - raise ValueError("Unsupported verkey derivation code = {}." - "".format(verfer.code)) + raise InvalidValueError(f"Unsupported verkey derivation code =" + f" {verfer.code}.") # convert signing public key to encryption public key raw = pysodium.crypto_sign_pk_to_box_pk(verfer.raw) @@ -776,7 +777,7 @@ def __init__(self, raw=None, code=MtrDex.X25519, verkey=None, **kwa): if self.code == MtrDex.X25519: self._encrypt = self._x25519 else: - raise ValueError("Unsupported encrypter code = {}.".format(self.code)) + raise InvalidValueError(f"Unsupported encrypter code = {self.code}.") def verifySeed(self, seed): """ @@ -807,23 +808,55 @@ def encrypt(self, ser=None, prim=None, code=None): qb64 or qb2 is to be encrypted based on code code (str): code of plain text type for resultant encrypted cipher """ - if not (ser or prim): - raise EmptyMaterialError(f"Neither bar serialization or primitive " - f"are provided.") + if not ser: - if ser: - prim = Matter(qb64b=ser) + if not prim: + raise EmptyMaterialError(f"Neither bar serialization or primitive " + f"are provided.") - if prim.code == MtrDex.Salt_128: # future other salt codes - code = MtrDex.X25519_Cipher_Salt - elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes - code = MtrDex.X25519_Cipher_Seed - else: - raise ValueError("Unsupported plain text code = {}.".format(prim.code)) + if not code: + if prim.code == MtrDex.Salt_128: # future other salt codes + code = MtrDex.X25519_Cipher_Salt + elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes + code = MtrDex.X25519_Cipher_Seed + else: + raise InvalidValueError(f"Unsupported primitive with code =" + f" {prim.code} when cipher code is " + f"missing.") + + if code in CiXAllQB64Dex: + ser = prim.qb64b + elif code in CiXVarQB2Dex: + ser = prim.qb2 + else: + raise InvalidCodeError(f"Invalide primitive cipher {code=} not " + f"qb64 or qb2.") + + if not code: # assumes default is sniffable stream + code = CiXDex.X25519_Cipher_L0 + + if hasattr(ser, "encode"): + ser = ser.encode() # convert str to bytes + if not isinstance(ser, bytes): + ser = bytes(ser) # convert bytearray and memoryview to bytes + + #if not (ser or prim): + #raise EmptyMaterialError(f"Neither bar serialization or primitive " + #f"are provided.") + + #if ser: + #prim = Matter(qb64b=ser) + + #if prim.code == MtrDex.Salt_128: # future other salt codes + #code = MtrDex.X25519_Cipher_Salt + #elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes + #code = MtrDex.X25519_Cipher_Seed + #else: + #raise ValueError("Unsupported plain text code = {}.".format(prim.code)) # encrypting fully qualified qb64 version of plain text ensures its # derivation code round trips through eventual decryption - return (self._encrypt(ser=prim.qb64b, pubkey=self.raw, code=code)) + return (self._encrypt(ser=ser, pubkey=self.raw, code=code)) @staticmethod def _x25519(ser, pubkey, code): diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index 9b8ec4d16..ef616b5a9 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -779,12 +779,12 @@ def test_encrypter(): assert encrypter.raw == pubkey assert encrypter.verifySeed(seed=cryptsigner.qb64) - cipher = encrypter.encrypt(ser=seedqb64b) + cipher = encrypter.encrypt(ser=seedqb64b, code=MtrDex.X25519_Cipher_Seed) assert cipher.code == MtrDex.X25519_Cipher_Seed uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) assert uncb == seedqb64b - cipher = encrypter.encrypt(ser=saltqb64b) + cipher = encrypter.encrypt(ser=saltqb64b, code=MtrDex.X25519_Cipher_Salt) assert cipher.code == MtrDex.X25519_Cipher_Salt uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) assert uncb == saltqb64b @@ -857,7 +857,7 @@ def test_decrypter(): assert encrypter.raw == pubkey # create cipher of seed - seedcipher = encrypter.encrypt(ser=seedqb64b) + seedcipher = encrypter.encrypt(ser=seedqb64b, code=MtrDex.X25519_Cipher_Seed) assert seedcipher.code == MtrDex.X25519_Cipher_Seed # each encryption uses a nonce so not a stable representation for testing @@ -882,7 +882,7 @@ def test_decrypter(): assert signer.verfer.transferable # create cipher of salt - saltcipher = encrypter.encrypt(ser=saltqb64b) + saltcipher = encrypter.encrypt(ser=saltqb64b, code=MtrDex.X25519_Cipher_Salt) assert saltcipher.code == MtrDex.X25519_Cipher_Salt # each encryption uses a nonce so not a stable representation for testing From 25babd348d47975e0999fa068706d86a6e5ea968 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 3 Aug 2024 15:17:47 -0600 Subject: [PATCH 04/17] started refactoring Decrypter to support variable length codes --- src/keri/core/signing.py | 44 ++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index 3df201ec2..6623a2b5d 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -840,22 +840,9 @@ def encrypt(self, ser=None, prim=None, code=None): if not isinstance(ser, bytes): ser = bytes(ser) # convert bytearray and memoryview to bytes - #if not (ser or prim): - #raise EmptyMaterialError(f"Neither bar serialization or primitive " - #f"are provided.") - - #if ser: - #prim = Matter(qb64b=ser) - - #if prim.code == MtrDex.Salt_128: # future other salt codes - #code = MtrDex.X25519_Cipher_Salt - #elif prim.code == MtrDex.Ed25519_Seed: # future other seed codes - #code = MtrDex.X25519_Cipher_Seed - #else: - #raise ValueError("Unsupported plain text code = {}.".format(prim.code)) - - # encrypting fully qualified qb64 version of plain text ensures its - # derivation code round trips through eventual decryption + # encrypting fully qualified qb64 version of cesr primitive as plain + # text with proper cipher code ensures primitive round trip through eventual + # decryption. Likewise for sniffable stream with sniffible cipher code. return (self._encrypt(ser=ser, pubkey=self.raw, code=code)) @staticmethod @@ -866,7 +853,7 @@ def _x25519(ser, pubkey, code): ser (Union[bytes, str]): qb64b or qb64 serialization of seed or salt to be encrypted. pubkey (bytes): raw binary serialization of encryption public key - code (str): derivation code of serialized plain text seed or salt + code (str): cipher derivation code """ raw = pysodium.crypto_box_seal(ser, pubkey) return Cipher(raw=raw, code=code) @@ -934,7 +921,7 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): else: raise ValueError("Unsupported decrypter code = {}.".format(self.code)) - def decrypt(self, ser=None, cipher=None, transferable=False): + def decrypt(self, *, ser=None, cipher=None, klas=None, transferable=False): """ Returns: Salter or Signer instance derived from plain text decrypted from @@ -944,10 +931,14 @@ def decrypt(self, ser=None, cipher=None, transferable=False): encryption/decryption round trip. Parameters: - ser (Union[bytes,str]): qb64b or qb64 serialization of cipher text + ser (bytes | str): serialization of cipher text cipher (Cipher): optional Cipher instance when ser is None - transferable (bool): True means associated verfer of returned - signer is transferable. False means non-transferable + klas (Matter, Indexer, Streamer): Class used to create instance from + decrypted serialization. + transferable (bool): Modifier of Klas instance creation. When klas + is signer; + True means verfer of returned signer is transferable. + False means non-transferable """ if not (ser or cipher): raise EmptyMaterialError("Neither ser or cipher are provided.") @@ -957,10 +948,11 @@ def decrypt(self, ser=None, cipher=None, transferable=False): return (self._decrypt(cipher=cipher, prikey=self.raw, + klas=klas, transferable=transferable)) @staticmethod - def _x25519(cipher, prikey, transferable=False): + def _x25519(cipher, prikey, klas, transferable=False): """ Returns plain text as Salter or Signer instance depending on the cipher code and the embedded encrypted plain text derivation code. @@ -969,8 +961,12 @@ def _x25519(cipher, prikey, transferable=False): cipher (Cipher): instance of encrypted seed or salt prikey (bytes): raw binary decryption private key derived from signing seed or sigkey - transferable (bool): True means associated verfer of returned - signer is transferable. False means non-transferable + klas (Matter, Indexer, Streamer): Class used to create instance from + decrypted serialization. + transferable (bool): Modifier of Klas instance creation. When klas + is signer; + True means verfer of returned signer is transferable. + False means non-transferable """ pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) plain = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) # qb64b From 42d51ca74275dd1b104bb487b17d608ba9bee8e3 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 3 Aug 2024 15:24:45 -0600 Subject: [PATCH 05/17] more refactoring of Decrypter --- src/keri/core/signing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index 6623a2b5d..52d4f33a3 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -921,7 +921,7 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): else: raise ValueError("Unsupported decrypter code = {}.".format(self.code)) - def decrypt(self, *, ser=None, cipher=None, klas=None, transferable=False): + def decrypt(self, *, cipher=None, ser=None, klas=None, transferable=False): """ Returns: Salter or Signer instance derived from plain text decrypted from @@ -931,8 +931,8 @@ def decrypt(self, *, ser=None, cipher=None, klas=None, transferable=False): encryption/decryption round trip. Parameters: - ser (bytes | str): serialization of cipher text cipher (Cipher): optional Cipher instance when ser is None + ser (bytes | str): serialization of cipher text klas (Matter, Indexer, Streamer): Class used to create instance from decrypted serialization. transferable (bool): Modifier of Klas instance creation. When klas From bec32e05afb7a462f048ae96a152ef2764bb8cf8 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sun, 4 Aug 2024 10:16:36 -0600 Subject: [PATCH 06/17] some clarifying refactoring --- src/keri/core/coring.py | 12 ++++----- src/keri/core/signing.py | 58 ++++++++++++++++++++-------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index 5896ffe09..7e8ee813e 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -350,12 +350,12 @@ class MatterCodex: 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 plaintext lead size 0 - X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 1 - X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 2 - X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 0 - X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 1 - X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable plaintext 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 diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index 52d4f33a3..e3fabefc8 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -484,24 +484,24 @@ def signers(self, count=1, start=0, path="", **kwa): # Codes for for ciphers of variable sized sniffable QB2 or QB64 plain text @dataclass(frozen=True) -class CipherX25519VarSnifCodex: +class CipherX25519VarStrmCodex: """ CipherX25519VarCodex is codex all variable sized cipher bytes derivation codes - for sealed box encryped ciphertext. Plaintext is Sniffable QB2 or QB64. + for sealed box encryped ciphertext. Plaintext is Sniffable CESR Stream. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ - X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 0 - X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 1 - X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 2 - X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 0 - X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 1 - X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable plaintext 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 def __iter__(self): return iter(astuple(self)) -CiXVarSnifDex = CipherX25519VarSnifCodex() # Make instance +CiXVarStrmDex = CipherX25519VarStrmCodex() # Make instance # Codes for for ciphers of variable sized QB64 plain text @@ -593,16 +593,16 @@ def __iter__(self): class CipherX25519AllVarCodex: """ CipherX25519AllVarCodex is codex all variable size codes of cipher bytes - for sealed box encryped ciphertext. Plaintext maybe sniffable or qb64 or qb2. + for sealed box encryped ciphertext. Plaintext maybe sniffable CESR stream or qb64 or qb2. Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ - X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 0 - X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 1 - X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 2 - X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 0 - X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 1 - X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable plaintext 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 @@ -631,12 +631,12 @@ class CipherX25519AllCodex: Only provide defined codes. Undefined are left out so that inclusion(exclusion) via 'in' operator works. """ - X25519_Cipher_L0: str = '4C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 0 - X25519_Cipher_L1: str = '5C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 1 - X25519_Cipher_L2: str = '6C' # X25519 sealed box cipher bytes of sniffable plaintext lead size 2 - X25519_Cipher_Big_L0: str = '7AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 0 - X25519_Cipher_Big_L1: str = '8AAC' # X25519 sealed box cipher bytes of sniffable plaintext big lead size 1 - X25519_Cipher_Big_L2: str = '9AAC' # X25519 sealed box cipher bytes of sniffable plaintext 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_Seed: str = 'P' # X25519 sealed box 124 char qb64 Cipher of 44 char qb64 Seed X25519_Cipher_Salt: str = '1AAH' # X25519 sealed box 100 char qb64 Cipher of 24 char qb64 Salt X25519_Cipher_QB64_L0: str = '4D' # X25519 sealed box cipher bytes of QB64 plaintext lead size 0 @@ -691,13 +691,13 @@ def __init__(self, raw=None, code=None, **kwa): # code given by raw size. Otherwise provided code fixed or variable size # is handled by Matter superclass. if raw is not None and code is None: - if len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Salt): - code = MtrDex.X25519_Cipher_Salt - elif len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Seed): - code = MtrDex.X25519_Cipher_Seed - else: - raise InvalidSizeError(f"Unsupported fixed raw size" - f" {len(raw)} for {code=}.") + if len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Salt): + code = MtrDex.X25519_Cipher_Salt + elif len(raw) == Matter._rawSize(MtrDex.X25519_Cipher_Seed): + code = MtrDex.X25519_Cipher_Seed + else: + raise InvalidSizeError(f"Unsupported fixed raw size" + f" {len(raw)} for {code=}.") if hasattr(raw, "encode"): raw = raw.encode("utf-8") # ensure bytes not str From 359d61aaa7a0d24d85f0f839bc7dc8f035915584 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 10:58:33 -0600 Subject: [PATCH 07/17] Decrypter now supports all cipher codes including variable sizes of qb64, qb2 and sniffable streams --- src/keri/app/keeping.py | 6 +-- src/keri/core/coring.py | 10 ++-- src/keri/core/signing.py | 93 ++++++++++++++++++++++++++------------ src/keri/db/subing.py | 4 +- tests/app/test_keeping.py | 4 +- tests/core/test_signing.py | 10 ++-- 6 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index b1d2f1089..839744af4 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -804,7 +804,7 @@ def updateAeid(self, aeid, seed): # re-encrypt root salt secrets by prefix parameters .prms for keys, data in self.ks.prms.getItemIter(): # keys is tuple of pre qb64 if data.salt: - salter = self.decrypter.decrypt(ser=data.salt) + salter = self.decrypter.decrypt(qb64=data.salt) data.salt = (self.encrypter.encrypt(prim=salter).qb64 if self.encrypter else salter.qb64) self.ks.prms.pin(keys, val=data) @@ -889,7 +889,7 @@ def salt(self): """ salt = self.ks.gbls.get('salt') if self.decrypter: # given .decrypt secret salt must be encrypted in db - return self.decrypter.decrypt(ser=salt).qb64 + return self.decrypter.decrypt(qb64=salt).qb64 return salt @@ -1185,7 +1185,7 @@ def rotate(self, pre, ncodes=None, ncount=1, if self.aeid: if not self.decrypter: raise kering.DecryptError("Unauthorized decryption. Aeid but no decrypter.") - salt = self.decrypter.decrypt(ser=salt).qb64 + salt = self.decrypter.decrypt(qb64=salt).qb64 else: salt = core.Salter(qb64=salt).qb64 # ensures salt was unencrypted diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index 7e8ee813e..6d933b81c 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -795,9 +795,13 @@ def __init__(self, raw=None, code=MtrDex.Ed25519N, soft='', rize=None, 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 + 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 diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index e3fabefc8..d11e378eb 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -19,7 +19,8 @@ from .coring import (Tiers, ) from .coring import (SmallVrzDex, LargeVrzDex, Matter, MtrDex, Verfer, Cigar) -from .indexing import IdrDex, Siger +from .indexing import IdrDex, Indexer, Siger +from .streaming import Streamer DSS_SIG_MODE = "fips-186-3" @@ -682,6 +683,9 @@ class Cipher(Matter): def __init__(self, raw=None, code=None, **kwa): """ + Inherited Parameters: + (see Matter) + Parmeters: raw (bytes | str): cipher text (not plain text) code (str): cipher suite @@ -725,7 +729,7 @@ def decrypt(self, prikey=None, seed=None): signing key seed used to derive private decryption key """ decrypter = Decrypter(qb64b=prikey, seed=seed) - return decrypter.decrypt(ser=self.qb64b) + return decrypter.decrypt(qb64=self.qb64b) class Encrypter(Matter): @@ -799,11 +803,12 @@ def encrypt(self, ser=None, prim=None, code=None): """ Returns: Cipher instance of cipher text encryption of plain text serialization - provided by either ser or Matter instance when provided. + provided by either ser or prim as CESR primitive instance. Parameters: - ser (Union[bytes,str]): qb64b or qb64 serialization of plain text + ser (str | bytes | bytearray | memoryview): qb64b or qb64 + serialization of plain text prim (Matter | Indexer): CESR primitive instance whose serialization qb64 or qb2 is to be encrypted based on code code (str): code of plain text type for resultant encrypted cipher @@ -893,13 +898,14 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): """ Assign decrypting cipher suite function to ._decrypt - Parameters: See Matter for inheirted parameters - raw (bytes): private decryption key derived from seed (private signing key) - qb64b (bytes): fully qualified private decryption key - qb64 (str): fully qualified private decryption key + Inherited Parameters: + (see Matter) + + Parameters: See Matter for inherited parameters code (str): derivation code for private decryption key - seed (Union[bytes, str]): qb64b or qb64 of signing key seed used to - derive raw which is private decryption key + seed (str | bytes | bytearray | memoryview | None): qb64b or qb64 + of signing key seed used to derive raw which is private + decryption key """ try: super(Decrypter, self).__init__(code=code, **kwa) @@ -921,7 +927,9 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): else: raise ValueError("Unsupported decrypter code = {}.".format(self.code)) - def decrypt(self, *, cipher=None, ser=None, klas=None, transferable=False): + + def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, + transferable=False, **kwa): """ Returns: Salter or Signer instance derived from plain text decrypted from @@ -931,20 +939,29 @@ def decrypt(self, *, cipher=None, ser=None, klas=None, transferable=False): encryption/decryption round trip. Parameters: - cipher (Cipher): optional Cipher instance when ser is None - ser (bytes | str): serialization of cipher text - klas (Matter, Indexer, Streamer): Class used to create instance from + cipher (Cipher): instance. One of cipher, qb64, or qb2 required. + qb64 (str | bytes | bytearray | memoryview | None ): serialization + of cipher text as fully qualified base64. When str, encodes as + utf-8. When bytearray and strip in kwa is True then strips. + qb2 (bytes | bytearray | memoryview | None ): serialization + of cipher text as fully qualified base2. Strips when bytearray + and strip in kwa is True. + klas (Matter | Indexer | Streamer): Class used to create instance from decrypted serialization. - transferable (bool): Modifier of Klas instance creation. When klas - is signer; + transferable (bool): Modifier of Klas instance creation. + When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable """ - if not (ser or cipher): - raise EmptyMaterialError("Neither ser or cipher are provided.") + if not cipher: + if qb64: # create cipher from qb64 + cipher = Cipher(qb64b=qb64, **kwa) - if ser: # create cipher to ensure valid derivation code of material in ser - cipher = Cipher(qb64b=ser) + elif qb2: + cipher = Cipher(qb2=qb2, **kwa) + + else: + raise EmptyMaterialError(f"Need one of cipher, qb64, or qb2.") return (self._decrypt(cipher=cipher, prikey=self.raw, @@ -952,7 +969,7 @@ def decrypt(self, *, cipher=None, ser=None, klas=None, transferable=False): transferable=transferable)) @staticmethod - def _x25519(cipher, prikey, klas, transferable=False): + def _x25519(cipher, prikey, klas=None, transferable=False): """ Returns plain text as Salter or Signer instance depending on the cipher code and the embedded encrypted plain text derivation code. @@ -961,19 +978,35 @@ def _x25519(cipher, prikey, klas, transferable=False): cipher (Cipher): instance of encrypted seed or salt prikey (bytes): raw binary decryption private key derived from signing seed or sigkey - klas (Matter, Indexer, Streamer): Class used to create instance from - decrypted serialization. - transferable (bool): Modifier of Klas instance creation. When klas - is signer; + klas (Matter, Indexer, Streamer | None): Class used to create instance from + decrypted serialization. Default depends on cipher.code. + transferable (bool): Modifier of Klas instance creation. + When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable """ pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) plain = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) # qb64b # ensure raw plain text is qb64b or qb64 so its derivation code is round tripped - if cipher.code == MtrDex.X25519_Cipher_Salt: - return Salter(qb64b=plain) - elif cipher.code == MtrDex.X25519_Cipher_Seed: - return Signer(qb64b=plain, transferable=transferable) + + if not klas: + if cipher.code == CiXFixQB64Dex.X25519_Cipher_Salt: + #return Salter(qb64b=plain) + klas = Salter + elif cipher.code == CiXFixQB64Dex.X25519_Cipher_Seed: + #return Signer(qb64b=plain, transferable=transferable) + klas = Signer + elif cipher.code in CiXVarStrmDex: + klas = Streamer + else: + raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}" + f" when klas missing.") + + if cipher.code in CiXAllQB64Dex: + return klas(qb64b=plain, transferable=transferable) + elif cipher.code in CiXVarQB2Dex: + return klas(qb2=plain) + elif cipher.code in CiXVarStrmDex: + return klas(stream=plain) else: - raise ValueError("Unsupported cipher text code = {}.".format(cipher.code)) + raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}.") diff --git a/src/keri/db/subing.py b/src/keri/db/subing.py index 5d092b53c..ec4ae7780 100644 --- a/src/keri/db/subing.py +++ b/src/keri/db/subing.py @@ -1008,7 +1008,7 @@ def get(self, keys: Union[str, Iterable], decrypter: core.Decrypter = None): keys = self._tokeys(key) # verkey is last split if any verfer = coring.Verfer(qb64b=keys[-1]) # last split if decrypter: - return (decrypter.decrypt(ser=bytes(val), + return (decrypter.decrypt(qb64=bytes(val), transferable=verfer.transferable)) return (self.klas(qb64b=bytes(val), transferable=verfer.transferable)) @@ -1037,7 +1037,7 @@ def getItemIter(self, keys: Union[str, Iterable]=b"", ikeys = self._tokeys(key) # verkey is last split if any verfer = coring.Verfer(qb64b=ikeys[-1]) # last split if decrypter: - yield (ikeys, decrypter.decrypt(ser=bytes(val), + yield (ikeys, decrypter.decrypt(qb64=bytes(val), transferable=verfer.transferable)) else: yield (ikeys, self.klas(qb64b=bytes(val), diff --git a/tests/app/test_keeping.py b/tests/app/test_keeping.py index 0d638aef3..87474c493 100644 --- a/tests/app/test_keeping.py +++ b/tests/app/test_keeping.py @@ -1591,7 +1591,7 @@ def test_manager_with_aeid(): pp = manager.ks.prms.get(spre) assert pp.pidx == 0 assert pp.algo == keeping.Algos.salty - assert manager.decrypter.decrypt(ser=pp.salt).qb64 == salt + assert manager.decrypter.decrypt(qb64=pp.salt).qb64 == salt assert pp.stem == '' assert pp.tier == core.Tiers.low @@ -1674,7 +1674,7 @@ def test_manager_with_aeid(): pp = manager.ks.prms.get(spre) assert pp.pidx == 0 assert pp.algo == keeping.Algos.salty - assert manager.decrypter.decrypt(ser=pp.salt).qb64 == salt + assert manager.decrypter.decrypt(qb64=pp.salt).qb64 == salt assert pp.stem == '' assert pp.tier == core.Tiers.low diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index ef616b5a9..16ffc8a17 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -868,7 +868,7 @@ def test_decrypter(): assert decrypter.raw == prikey # decrypt seed cipher using ser - designer = decrypter.decrypt(ser=seedcipher.qb64b, transferable=signer.verfer.transferable) + designer = decrypter.decrypt(qb64=seedcipher.qb64b, transferable=signer.verfer.transferable) assert designer.qb64b == seedqb64b assert designer.code == MtrDex.Ed25519_Seed assert designer.verfer.code == MtrDex.Ed25519 @@ -887,7 +887,7 @@ def test_decrypter(): # each encryption uses a nonce so not a stable representation for testing # decrypt salt cipher using ser - desalter = decrypter.decrypt(ser=saltcipher.qb64b) + desalter = decrypter.decrypt(qb64=saltcipher.qb64b) assert desalter.qb64b == saltqb64b assert desalter.code == MtrDex.Salt_128 @@ -900,7 +900,7 @@ def test_decrypter(): # get from seedcipher above cipherseed = ('PM9jOGWNYfjM_oLXJNaQ8UlFSAV5ACjsUY7J16xfzrlpc9Ve3A5WYrZ4o_' 'NHtP5lhp78Usspl9fyFdnCdItNd5JyqZ6dt8SXOt6TOqOCs-gy0obrwFkPPqBvVkEw') - designer = decrypter.decrypt(ser=cipherseed, transferable=signer.verfer.transferable) + designer = decrypter.decrypt(qb64=cipherseed, transferable=signer.verfer.transferable) assert designer.qb64b == seedqb64b assert designer.code == MtrDex.Ed25519_Seed assert designer.verfer.code == MtrDex.Ed25519 @@ -909,7 +909,7 @@ def test_decrypter(): # get from saltcipher above ciphersalt = ('1AAHjlR2QR9J5Et67Wy-ZaVdTryN6T6ohg44r73GLRPnHw-5S3ABFkhWy' 'IwLOI6TXUB_5CT13S8JvknxLxBaF8ANPK9FSOPD8tYu') - desalter = decrypter.decrypt(ser=ciphersalt) + desalter = decrypter.decrypt(qb64=ciphersalt) assert desalter.qb64b == saltqb64b assert desalter.code == MtrDex.Salt_128 @@ -920,7 +920,7 @@ def test_decrypter(): assert decrypter.raw == prikey # decrypt ciphersalt - desalter = decrypter.decrypt(ser=saltcipher.qb64b) + desalter = decrypter.decrypt(qb64=saltcipher.qb64b) assert desalter.qb64b == saltqb64b assert desalter.code == MtrDex.Salt_128 From 187e4f1e19219498732232ed043b573744258ad9 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 11:15:58 -0600 Subject: [PATCH 08/17] cleaned up keeping.Manager.decrypt method --- src/keri/app/habbing.py | 5 ++--- src/keri/app/keeping.py | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/keri/app/habbing.py b/src/keri/app/habbing.py index d32a858f5..e67464835 100644 --- a/src/keri/app/habbing.py +++ b/src/keri/app/habbing.py @@ -1335,9 +1335,8 @@ def decrypt(self, ser, verfers=None, **kwa): if verfers is None: verfers = self.kever.verfers # when group these provide group signing keys - return self.mgr.decrypt(ser=ser, - verfers=verfers, - ) + return self.mgr.decrypt(qb64=ser, verfers=verfers) + def query(self, pre, src, query=None, **kwa): """ Create, sign and return a `qry` message against the attester for the prefix diff --git a/src/keri/app/keeping.py b/src/keri/app/keeping.py index 839744af4..57ac560be 100644 --- a/src/keri/app/keeping.py +++ b/src/keri/app/keeping.py @@ -1396,12 +1396,13 @@ def sign(self, ser, pubs=None, verfers=None, indexed=True, return cigars - def decrypt(self, ser, pubs=None, verfers=None): + def decrypt(self, qb64, pubs=None, verfers=None): """ - Returns plain text of decrypted serialization + Returns decrypted plaintext of encrypted qb64 ciphertext serialization. Parameters: - ser (bytes): serialization to sign + qb64 (str | bytes | bytearray | memoryview): fully qualified base64 + ciphertext serialization to decrypt pubs (list[str] | None): of qb64 public keys to lookup private keys one of pubs or verfers is required. If both then verfers is ignored. verfers (list[Verfer] | None): Verfer instances of public keys @@ -1410,7 +1411,7 @@ def decrypt(self, ser, pubs=None, verfers=None): private keys Returns: - bytes: decrypted data + plain (bytes): decrypted plaintext """ signers = [] @@ -1435,18 +1436,22 @@ def decrypt(self, ser, pubs=None, verfers=None): raise ValueError("Missing prikey in db for pubkey={}".format(verfer.qb64)) signers.append(signer) - plain = ser + if hasattr(qb64, "encode"): + qb64 = qb64.encode() # convert str to bytes + qb64 = bytes(qb64) # convert bytearray or memoryview to bytes + for signer in signers: sigkey = signer.raw + signer.verfer.raw # sigkey is raw seed + raw verkey prikey = pysodium.crypto_sign_sk_to_box_sk(sigkey) # raw private encrypt key pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) - plain = pysodium.crypto_box_seal_open(plain, pubkey, prikey) # qb64b + plain = pysodium.crypto_box_seal_open(qb64, pubkey, prikey) # qb64b - if plain == ser: - raise ValueError("unable to decrypt data") + if plain == qb64: + raise ValueError(f"Unable to decrypt.") return plain + def ingest(self, secrecies, iridx=0, ncount=1, ncode=coring.MtrDex.Ed25519_Seed, dcode=coring.MtrDex.Blake3_256, algo=Algos.salty, salt=None, stem=None, tier=None, From 370a84210b965cafee6c1aaa5f4d0d3d26e86dfe Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 11:33:46 -0600 Subject: [PATCH 09/17] clean up hab.decrypt. Needs more work --- src/keri/app/cli/commands/decrypt.py | 4 ++-- src/keri/app/cli/commands/witness/authenticate.py | 4 ++-- src/keri/app/habbing.py | 6 +++++- tests/peer/test_exchanging.py | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/keri/app/cli/commands/decrypt.py b/src/keri/app/cli/commands/decrypt.py index de460ded8..063242c4d 100644 --- a/src/keri/app/cli/commands/decrypt.py +++ b/src/keri/app/cli/commands/decrypt.py @@ -55,8 +55,8 @@ def decrypt(tymth, tock=0.0, **opts): else: data = data - m = coring.Matter(qb64=data) - d = coring.Matter(qb64=hab.decrypt(m.raw)) + m = coring.Matter(qb64=data) # should refactor this to use Cipher + d = coring.Matter(qb64=hab.decrypt(ser=m.raw)) print(d.raw) except kering.ConfigurationError: diff --git a/src/keri/app/cli/commands/witness/authenticate.py b/src/keri/app/cli/commands/witness/authenticate.py index 1758093ce..f0a2b4192 100644 --- a/src/keri/app/cli/commands/witness/authenticate.py +++ b/src/keri/app/cli/commands/witness/authenticate.py @@ -127,8 +127,8 @@ def authDo(self, tymth, tock=0.0): data = json.loads(rep.body) totp = data["totp"] - m = coring.Matter(qb64=totp) - d = coring.Matter(qb64=self.hab.decrypt(m.raw)) + m = coring.Matter(qb64=totp) # refactor this to use cipher + d = coring.Matter(qb64=self.hab.decrypt(ser=m.raw)) otpurl = f"otpauth://totp/KERIpy:{self.witness}?secret={d.raw.decode('utf-8')}&issuer=KERIpy" if not self.urlOnly: diff --git a/src/keri/app/habbing.py b/src/keri/app/habbing.py index e67464835..d9431aa4a 100644 --- a/src/keri/app/habbing.py +++ b/src/keri/app/habbing.py @@ -1320,12 +1320,14 @@ def sign(self, ser, verfers=None, indexed=True, indices=None, ondices=None, **kw indices=indices, ondices=ondices) + def decrypt(self, ser, verfers=None, **kwa): """Decrypt given serialization ser using appropriate keys. Use provided verfers or .kever.verfers to lookup keys to sign. Parameters: - ser (bytes): serialization to decrypt + ser (str | bytes | bytearray | memoryview): serialization to decrypt + verfers (list[Verfer] | None): Verfer instances to get pub verifier keys to lookup and convert to private decryption keys. verfers None means use .kever.verfers. Assumes that when group @@ -1335,6 +1337,8 @@ def decrypt(self, ser, verfers=None, **kwa): if verfers is None: verfers = self.kever.verfers # when group these provide group signing keys + # should not use mgr.decrypt since it assumes qb64. Just lucky its not + # yet a problem return self.mgr.decrypt(qb64=ser, verfers=verfers) diff --git a/tests/peer/test_exchanging.py b/tests/peer/test_exchanging.py index 54115ae60..4a0a0a009 100644 --- a/tests/peer/test_exchanging.py +++ b/tests/peer/test_exchanging.py @@ -84,7 +84,7 @@ def test_essrs(): # Pull the logged ESSR attachment and verify it is the one attached texter = recHby.db.essrs.get(keys=(serder.said,)) - raw = recHab.decrypt(texter[0].raw) + raw = recHab.decrypt(ser=texter[0].raw) assert raw.decode("utf-8") == msg # Test with invalid diger From da477797fe5dda74531d91af53b46bb04e6f8894 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 11:59:43 -0600 Subject: [PATCH 10/17] fix Cipher.decrypt to support new Decrypter.decrypt --- src/keri/core/signing.py | 58 ++++++++++++++++++++++++++++---------- src/keri/core/streaming.py | 14 +++++++-- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index d11e378eb..c1edb91d6 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -712,24 +712,44 @@ def __init__(self, raw=None, code=None, **kwa): raise InvalidCodeError(f"Unsupported cipher code = {self.code}.") - def decrypt(self, prikey=None, seed=None): + def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, + **kwa): """ - Returns plain text as Matter instance (Signer or Salter) of cryptographic - cipher text material given by .raw. Encrypted plain text is fully - qualified (qb64) so derivaton code of plain text preserved through + Returns plain text as klas instance (Matter, Indexer, Streamer). + When klas is None then klas default is based on .code. Maybe Salter, + Signer, or Streamer. Encrypted plain text is fully + qualified (qb64) via self so derivaton code of plain text preserved through encryption/decryption round trip. - Decrypter uses either decryption key given by prikey or derives prikey from - signing key derived from private seed. + The created Decrypter uses either decryption key given by prikey or + when prikey missing derives prikey from signing key derived from private + seed. + + Returns: + decrypted (Matter | Indexer | Streamer): instance of decrypted + cipher text of .raw which is encrypted qb64, qb2, or sniffable + stream depending on .code + + Keyword Parameters: + (see Matter because created Decrypter is Matter subclass) Parameters: prikey (Union[bytes, str]): qb64b or qb64 serialization of private decryption key seed (Union[bytes, str]): qb64b or qb64 serialization of private signing key seed used to derive private decryption key + klas (Matter | Indexer | Streamer): Class used to create instance from + decrypted serialization. + transferable (bool): Modifier of klas instance creation. + When klas init (such as Signer) supports transferabe parm; + True means verfer of returned signer is transferable. + False means non-transferable """ - decrypter = Decrypter(qb64b=prikey, seed=seed) - return decrypter.decrypt(qb64=self.qb64b) + decrypter = Decrypter(qb64b=prikey, seed=seed, **kwa) + return decrypter.decrypt(cipher=self, klas=klas, transferable=transferable) + + #return decrypter.decrypt(qb64=self.qb64b) + class Encrypter(Matter): @@ -930,13 +950,21 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, transferable=False, **kwa): - """ + """Returns plain text as klas instance (Matter, Indexer, Streamer). + When klas is None then klas default is based on cipher.code or inferred + from qb64 or qb2 code. Default maybe Salter, Signer, or Streamer. + Cipher's encrypted plain text is fully qualified (qb64) + so derivaton code of plain text preserved through encryption/decryption + round trip. + + Returns: - Salter or Signer instance derived from plain text decrypted from - encrypted cipher text material given by ser or cipher. Plain text - that is orignally encrypt should always be fully qualified (qb64b) - so that derivaton code of plain text is preserved through - encryption/decryption round trip. + decrypted (Matter | Indexer | Streamer): instance of decrypted + cipher text of .raw which is encrypted qb64, qb2, or sniffable + stream depending on .code + + Keyword Parameters: + (see Matter because created Decrypter is Matter subclass) Parameters: cipher (Cipher): instance. One of cipher, qb64, or qb2 required. @@ -948,7 +976,7 @@ def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, and strip in kwa is True. klas (Matter | Indexer | Streamer): Class used to create instance from decrypted serialization. - transferable (bool): Modifier of Klas instance creation. + transferable (bool): Modifier of klas instance creation. When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable diff --git a/src/keri/core/streaming.py b/src/keri/core/streaming.py index 4ad253ecc..be88485c9 100644 --- a/src/keri/core/streaming.py +++ b/src/keri/core/streaming.py @@ -340,7 +340,7 @@ class Streamer: """ - def __init__(self, stream): + def __init__(self, stream, verify=False): """Initialize instance Holds sniffable CESR stream as byte like string either (bytes, bytearray, or memoryview) @@ -348,7 +348,7 @@ def __init__(self, stream): Parameters: stream (str | bytes | bytearray | memoryview): sniffable CESR stream - + verify (bool): When True raise error if .stream is not sniffable. """ @@ -359,6 +359,15 @@ def __init__(self, stream): self._stream = stream + @property + def _verify(self): + """Returns True if sniffable stream, False otherwise + Returns: + sniffable (bool): True when .stream is sniffable. + False otherwise. + """ + return False + @property def stream(self): @@ -366,6 +375,7 @@ def stream(self): """ return self._stream + @property def text(self): """expanded stream where all primitives and groups in stream are From 40ef9c8dd93caea872089acd751d6e11bff8eea6 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 13:18:26 -0600 Subject: [PATCH 11/17] unit tests for Cipher.decrypt with variable sized cipher codes of all three types QB64, QB2, and Sniffable Stream --- src/keri/core/__init__.py | 1 + src/keri/core/signing.py | 9 +- tests/core/test_signing.py | 289 +++++++++++++++++++++++++++++++++---- 3 files changed, 263 insertions(+), 36 deletions(-) diff --git a/src/keri/core/__init__.py b/src/keri/core/__init__.py index ec83d8bbb..b37b9516c 100644 --- a/src/keri/core/__init__.py +++ b/src/keri/core/__init__.py @@ -16,3 +16,4 @@ from .indexing import Indexer, Siger, IdrDex, IdxSigDex from .signing import Signer, Salter, Cipher, CiXDex, Encrypter, Decrypter from .counting import Counter, Codens, CtrDex_2_0 +from .streaming import Streamer diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index c1edb91d6..f8aeb6ea8 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -734,10 +734,11 @@ def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, (see Matter because created Decrypter is Matter subclass) Parameters: - prikey (Union[bytes, str]): qb64b or qb64 serialization of private - decryption key - seed (Union[bytes, str]): qb64b or qb64 serialization of private - signing key seed used to derive private decryption key + prikey (str | bytes): qb64 or qb64b serialization of private + decryption key. Must be fully qualified with code. + seed (str | bytes): qb64 or qb64b serialization of private + signing key seed used to derive private decryption key. Must be + fully qualified with code. klas (Matter | Indexer | Streamer): Class used to create instance from decrypted serialization. transferable (bool): Modifier of klas instance creation. diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index 16ffc8a17..1d07fd6d4 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -642,18 +642,18 @@ def test_cipher(): texter = core.Texter(text=plain) assert texter.text == plain counter = core.Counter(core.Codens.GenericGroup, count=texter.size) - peb = counter.qb64b + texter.qb64b - raw = pysodium.crypto_box_seal(peb, pubkey) # uses nonce so different everytime + strm = counter.qb64b + texter.qb64b + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime assert len(raw) == 108 assert (3 - (len(raw) % 3)) % 3 == 0 cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) assert cipher.code == CiXDex.X25519_Cipher_L0 uncb = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) - assert uncb == peb - peb = bytearray(peb) - counter = core.Counter(qb64b=peb, strip=True) + assert uncb == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup - texter = core.Texter(qb64b=peb, strip=True) + texter = core.Texter(qb64b=strm, strip=True) assert texter.text == plain # sniffable qb64 lead 1 @@ -661,18 +661,18 @@ def test_cipher(): texter = core.Texter(text=plain) assert texter.text == plain counter = core.Counter(core.Codens.GenericGroup, count=texter.size) - peb = counter.qb64b + texter.qb64b - raw = pysodium.crypto_box_seal(peb, pubkey) # uses nonce so different everytime + strm = counter.qb64b + texter.qb64b + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime assert len(raw) == 116 assert (3 - (len(raw) % 3)) % 3 == 1 cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) assert cipher.code == CiXDex.X25519_Cipher_L1 uncb = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) - assert uncb == peb - peb = bytearray(peb) - counter = core.Counter(qb64b=peb, strip=True) + assert uncb == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup - texter = core.Texter(qb64b=peb, strip=True) + texter = core.Texter(qb64b=strm, strip=True) assert texter.text == plain # sniffable qb64 lead 2 @@ -680,18 +680,18 @@ def test_cipher(): texter = core.Texter(text=plain) assert texter.text == plain counter = core.Counter(core.Codens.GenericGroup, count=texter.size) - peb = counter.qb64b + texter.qb64b - raw = pysodium.crypto_box_seal(peb, pubkey) # uses nonce so different everytime + strm = counter.qb64b + texter.qb64b + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime assert len(raw) == 112 assert (3 - (len(raw) % 3)) % 3 == 2 cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) assert cipher.code == CiXDex.X25519_Cipher_L2 uncb = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) - assert uncb == peb - peb = bytearray(peb) - counter = core.Counter(qb64b=peb, strip=True) + assert uncb == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup - texter = core.Texter(qb64b=peb, strip=True) + texter = core.Texter(qb64b=strm, strip=True) assert texter.text == plain # sniffable qb2 lead 0 @@ -700,19 +700,19 @@ def test_cipher(): assert texter.text == plain counter = core.Counter(core.Codens.GenericGroup, count=texter.size) assert counter.code == core.CtrDex_2_0.GenericGroup - peb = counter.qb64b + texter.qb64b - pebqb2 = decodeB64(peb) - raw = pysodium.crypto_box_seal(pebqb2, pubkey) # uses nonce so different everytime + strm = counter.qb64b + texter.qb64b + strmb2 = decodeB64(strm) + raw = pysodium.crypto_box_seal(strmb2, pubkey) # uses nonce so different everytime assert len(raw) == 93 assert (3 - (len(raw) % 3)) % 3 == 0 cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) assert cipher.code == CiXDex.X25519_Cipher_L0 uncb = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) - assert uncb == pebqb2 - pebqb2 = bytearray(pebqb2) - counter = core.Counter(qb2=pebqb2, strip=True) + assert uncb == strmb2 + strmb2 = bytearray(strmb2) + counter = core.Counter(qb2=strmb2, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup - texter = core.Texter(qb2=pebqb2, strip=True) + texter = core.Texter(qb2=strmb2, strip=True) assert texter.text == plain @@ -723,20 +723,245 @@ def test_cipher(): assert texter.text == plain counter = core.Counter(core.Codens.GenericGroup, count=texter.size) assert counter.code == core.CtrDex_2_0.BigGenericGroup - peb = counter.qb64b + texter.qb64b - pebqb2 = decodeB64(peb) - raw = pysodium.crypto_box_seal(pebqb2, pubkey) # uses nonce so different everytime + strm = counter.qb64b + texter.qb64b + strmb2 = decodeB64(strm) + raw = pysodium.crypto_box_seal(strmb2, pubkey) # uses nonce so different everytime assert len(raw) == 12696 assert (3 - (len(raw) % 3)) % 3 == 0 assert (len(raw) // 3 ) > (64 ** 2 - 1) # triplets cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) assert cipher.code == CiXDex.X25519_Cipher_Big_L0 uncb = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) - assert uncb == pebqb2 - pebqb2 = bytearray(pebqb2) - counter = core.Counter(qb2=pebqb2, strip=True) + assert uncb == strmb2 + strmb2 = bytearray(strmb2) + counter = core.Counter(qb2=strmb2, strip=True) assert counter.code == core.CtrDex_2_0.BigGenericGroup - texter = core.Texter(qb2=pebqb2, strip=True) + texter = core.Texter(qb2=strmb2, strip=True) + assert texter.text == plain + + # test .decrypt method with variable sized qb64 coded ciphers + # qb64 lead 0 + plain = "The quick brown fox jumps over the lazy " + texter = core.Texter(text=plain) + assert texter.text == plain + raw = pysodium.crypto_box_seal(texter.qb64b, pubkey) # uses nonce so different everytime + assert len(raw) == 108 + assert (3 - (len(raw) % 3)) % 3 == 0 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_QB64_L0) + assert cipher.code == CiXDex.X25519_Cipher_QB64_L0 + # test using prikey + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(prikey=prikeyqb64) + texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) + assert texter.text == plain + # test using seed + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(seed=cryptseedqb64) + texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) + assert texter.text == plain + + + # qb64 lead 1 + plain = "The quick brown fox jumps over the lazy dogcats" + texter = core.Texter(text=plain) + assert texter.text == plain + raw = pysodium.crypto_box_seal(texter.qb64b, pubkey) # uses nonce so different everytime + assert len(raw) == 116 + assert (3 - (len(raw) % 3)) % 3 == 1 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_QB64_L0) + assert cipher.code == CiXDex.X25519_Cipher_QB64_L1 + # test using prikey + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(prikey=prikeyqb64) + texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) + assert texter.text == plain + # test using seed + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(seed=cryptseedqb64) + texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) + assert texter.text == plain + + + # qb64 lead 2 + plain = "The quick brown fox jumps over the lazy dog" + texter = core.Texter(text=plain) + assert texter.text == plain + raw = pysodium.crypto_box_seal(texter.qb64b, pubkey) # uses nonce so different everytime + assert len(raw) == 112 + assert (3 - (len(raw) % 3)) % 3 == 2 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_QB64_L0) + assert cipher.code == CiXDex.X25519_Cipher_QB64_L2 + # test using prikey + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(prikey=prikeyqb64) + texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) + assert texter.text == plain + # test using seed + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(seed=cryptseedqb64) + texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) + assert texter.text == plain + + # test .decrypt method with variable sized qb2 coded ciphers + # qb2 lead 0 (always lead 0 when qb2 from texter) + plain = "The quick brown fox jumps over the lazy dog" + texter = core.Texter(text=plain) + assert texter.text == plain + raw = pysodium.crypto_box_seal(texter.qb2, pubkey) # uses nonce so different everytime + assert len(raw) == 96 + assert (3 - (len(raw) % 3)) % 3 == 0 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_QB2_L0) + assert cipher.code == CiXDex.X25519_Cipher_QB2_L0 + # test using prikey + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(prikey=prikeyqb64) + texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) + assert texter.text == plain + # test using seed + with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 + result = cipher.decrypt(seed=cryptseedqb64) + texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) + assert texter.text == plain + + # sniffable qb64 lead 0 + plain = "The quick brown fox jumps over the lazy" + texter = core.Texter(text=plain) + assert texter.text == plain + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + strm = counter.qb64b + texter.qb64b # sniffable stream + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime + assert len(raw) == 108 + assert (3 - (len(raw) % 3)) % 3 == 0 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L0 + # test using prikey + streamer = cipher.decrypt(prikey=prikeyqb64) # default klas is streamer + assert streamer.stream == strm + streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) + assert streamer.stream == strm + # test using seed + streamer = cipher.decrypt(seed=cryptseedqb64) + assert streamer.stream == strm + streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) + assert streamer.stream == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) + assert counter.code == core.CtrDex_2_0.GenericGroup + texter = core.Texter(qb64b=strm, strip=True) + assert texter.text == plain + + # sniffable qb64 lead 1 + plain = "The quick brown fox jumps over the lazy dog" + texter = core.Texter(text=plain) + assert texter.text == plain + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + strm = counter.qb64b + texter.qb64b + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime + assert len(raw) == 116 + assert (3 - (len(raw) % 3)) % 3 == 1 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L1 + # test using prikey + streamer = cipher.decrypt(prikey=prikeyqb64) # default klas is streamer + assert streamer.stream == strm + streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) + assert streamer.stream == strm + # test using seed + streamer = cipher.decrypt(seed=cryptseedqb64) + assert streamer.stream == strm + streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) + assert streamer.stream == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) + assert counter.code == core.CtrDex_2_0.GenericGroup + texter = core.Texter(qb64b=strm, strip=True) + assert texter.text == plain + + # sniffable qb64 lead 2 + plain = "The quick brown fox jumps over the lazy " + texter = core.Texter(text=plain) + assert texter.text == plain + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + strm = counter.qb64b + texter.qb64b + raw = pysodium.crypto_box_seal(strm, pubkey) # uses nonce so different everytime + assert len(raw) == 112 + assert (3 - (len(raw) % 3)) % 3 == 2 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L2 + # test using prikey + streamer = cipher.decrypt(prikey=prikeyqb64) # default klas is streamer + assert streamer.stream == strm + streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) + assert streamer.stream == strm + # test using seed + streamer = cipher.decrypt(seed=cryptseedqb64) + assert streamer.stream == strm + streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) + assert streamer.stream == strm + strm = bytearray(strm) + counter = core.Counter(qb64b=strm, strip=True) + assert counter.code == core.CtrDex_2_0.GenericGroup + texter = core.Texter(qb64b=strm, strip=True) + assert texter.text == plain + + # sniffable qb2 lead 0 + plain = "The quick brown fox jumps over the lazy" + texter = core.Texter(text=plain) + assert texter.text == plain + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + assert counter.code == core.CtrDex_2_0.GenericGroup + strm = counter.qb64b + texter.qb64b + strmb2 = decodeB64(strm) + raw = pysodium.crypto_box_seal(strmb2, pubkey) # uses nonce so different everytime + assert len(raw) == 93 + assert (3 - (len(raw) % 3)) % 3 == 0 + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L0 + # test using prikey + streamer = cipher.decrypt(prikey=prikeyqb64) # default klas is streamer + assert streamer.stream == strmb2 + streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) + assert streamer.stream == strmb2 + # test using seed + streamer = cipher.decrypt(seed=cryptseedqb64) + assert streamer.stream == strmb2 + streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) + assert streamer.stream == strmb2 + strmb2 = bytearray(strmb2) + counter = core.Counter(qb2=strmb2, strip=True) + assert counter.code == core.CtrDex_2_0.GenericGroup + texter = core.Texter(qb2=strmb2, strip=True) + assert texter.text == plain + + # sniffable qb2 lead 0 Big + + plain = "The quick brown fox jumps over the lazy" * 324 + texter = core.Texter(text=plain) + assert texter.text == plain + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + assert counter.code == core.CtrDex_2_0.BigGenericGroup + strm = counter.qb64b + texter.qb64b + strmb2 = decodeB64(strm) + raw = pysodium.crypto_box_seal(strmb2, pubkey) # uses nonce so different everytime + assert len(raw) == 12696 + assert (3 - (len(raw) % 3)) % 3 == 0 + assert (len(raw) // 3 ) > (64 ** 2 - 1) # triplets + cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_Big_L0 + # test using prikey + streamer = cipher.decrypt(prikey=prikeyqb64) # default klas is streamer + assert streamer.stream == strmb2 + streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) + assert streamer.stream == strmb2 + # test using seed + streamer = cipher.decrypt(seed=cryptseedqb64) + assert streamer.stream == strmb2 + streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) + assert streamer.stream == strmb2 + strmb2 = bytearray(strmb2) + counter = core.Counter(qb2=strmb2, strip=True) + assert counter.code == core.CtrDex_2_0.BigGenericGroup + texter = core.Texter(qb2=strmb2, strip=True) assert texter.text == plain """ Done Test """ From fc11bc04410508cd599ede9c38d52ce3921db0d9 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 13:38:27 -0600 Subject: [PATCH 12/17] set up for round trip encrypt decrypt unit tests using Encrypter, Cipher, and Decrypter --- src/keri/core/signing.py | 5 --- tests/core/test_signing.py | 62 ++++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index f8aeb6ea8..e05a9a61a 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -749,9 +749,6 @@ def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, decrypter = Decrypter(qb64b=prikey, seed=seed, **kwa) return decrypter.decrypt(cipher=self, klas=klas, transferable=transferable) - #return decrypter.decrypt(qb64=self.qb64b) - - class Encrypter(Matter): """ @@ -1020,10 +1017,8 @@ def _x25519(cipher, prikey, klas=None, transferable=False): if not klas: if cipher.code == CiXFixQB64Dex.X25519_Cipher_Salt: - #return Salter(qb64b=plain) klas = Salter elif cipher.code == CiXFixQB64Dex.X25519_Cipher_Seed: - #return Signer(qb64b=plain, transferable=transferable) klas = Signer elif cipher.code in CiXVarStrmDex: klas = Streamer diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index 1d07fd6d4..8de9a64f6 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -1004,16 +1004,6 @@ def test_encrypter(): assert encrypter.raw == pubkey assert encrypter.verifySeed(seed=cryptsigner.qb64) - cipher = encrypter.encrypt(ser=seedqb64b, code=MtrDex.X25519_Cipher_Seed) - assert cipher.code == MtrDex.X25519_Cipher_Seed - uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) - assert uncb == seedqb64b - - cipher = encrypter.encrypt(ser=saltqb64b, code=MtrDex.X25519_Cipher_Salt) - assert cipher.code == MtrDex.X25519_Cipher_Salt - uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) - assert uncb == saltqb64b - verfer = Verfer(raw=verkey, code=MtrDex.Ed25519) encrypter = Encrypter(verkey=verfer.qb64) @@ -1032,6 +1022,24 @@ def test_encrypter(): assert encrypter.code == MtrDex.X25519 assert encrypter.qb64 == 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' assert encrypter.raw == pubkey + + # Test encrypt method + encrypter = Encrypter(raw=pubkey) + assert encrypter.code == MtrDex.X25519 + assert encrypter.qb64 == 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' + assert encrypter.raw == pubkey + assert encrypter.verifySeed(seed=cryptsigner.qb64) + + cipher = encrypter.encrypt(ser=seedqb64b, code=MtrDex.X25519_Cipher_Seed) + assert cipher.code == MtrDex.X25519_Cipher_Seed + uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) + assert uncb == seedqb64b + + cipher = encrypter.encrypt(ser=saltqb64b, code=MtrDex.X25519_Cipher_Salt) + assert cipher.code == MtrDex.X25519_Cipher_Salt + uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) + assert uncb == saltqb64b + """ Done Test """ @@ -1149,8 +1157,41 @@ def test_decrypter(): assert desalter.qb64b == saltqb64b assert desalter.code == MtrDex.Salt_128 + + """ Done Test """ +def test_roundtrip(): + """Test round trip encrypt decrypt with variable sized ciphers""" + + # cryptseed = pysodium.randombytes(pysodium.crypto_box_SEEDBYTES) + cryptseed = b'h,#|\x8ap"\x12\xc43t2\xa6\xe1\x18\x19\xf0f2,y\xc4\xc21@\xf5@\x15.\xa2\x1a\xcf' + verkey, sigkey = pysodium.crypto_sign_seed_keypair(cryptseed) # raw + pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey) + prikey = pysodium.crypto_sign_sk_to_box_sk(sigkey) + + # create decrypter from prikey + decrypter = Decrypter(raw=prikey) + assert decrypter.code == MtrDex.X25519_Private + assert decrypter.qb64 == 'OLCFxqMz1z1UUS0TEJnvZP_zXHcuYdQsSGBWdOZeY5VQ' + assert decrypter.raw == prikey + + # create encrypter from pubkey + encrypter = Encrypter(raw=pubkey) + assert encrypter.code == MtrDex.X25519 + assert encrypter.qb64 == 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' + assert encrypter.raw == pubkey + + # create cipher using Encrypter + + + # decrypt cipher using Decrypter + + + + + """End Test""" + if __name__ == "__main__": @@ -1161,4 +1202,5 @@ def test_decrypter(): test_cipher() test_encrypter() test_decrypter() + test_roundtrip() From e41b0f99aa026cd5d4da0edb22a6ff66b827f4c9 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 14:22:13 -0600 Subject: [PATCH 13/17] added bare parameter to decrypt methods need unit tests --- src/keri/core/signing.py | 72 +++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index e05a9a61a..254986c9e 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -713,7 +713,7 @@ def __init__(self, raw=None, code=None, **kwa): def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, - **kwa): + bare=False, **kwa): """ Returns plain text as klas instance (Matter, Indexer, Streamer). When klas is None then klas default is based on .code. Maybe Salter, @@ -728,7 +728,8 @@ def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, Returns: decrypted (Matter | Indexer | Streamer): instance of decrypted cipher text of .raw which is encrypted qb64, qb2, or sniffable - stream depending on .code + stream depending on .code when bare is False. Otherwise returns + plaintext itself. Keyword Parameters: (see Matter because created Decrypter is Matter subclass) @@ -745,9 +746,14 @@ def decrypt(self, prikey=None, seed=None, klas=None, transferable=False, When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable + bare (bool): False (default) means returns instance holding plaintext + True means returns plaintext itself """ decrypter = Decrypter(qb64b=prikey, seed=seed, **kwa) - return decrypter.decrypt(cipher=self, klas=klas, transferable=transferable) + return decrypter.decrypt(cipher=self, + klas=klas, + transferable=transferable, + bare=bare) class Encrypter(Matter): @@ -947,7 +953,7 @@ def __init__(self, code=MtrDex.X25519_Private, seed=None, **kwa): def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, - transferable=False, **kwa): + transferable=False, bare=False, **kwa): """Returns plain text as klas instance (Matter, Indexer, Streamer). When klas is None then klas default is based on cipher.code or inferred from qb64 or qb2 code. Default maybe Salter, Signer, or Streamer. @@ -957,9 +963,11 @@ def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, Returns: - decrypted (Matter | Indexer | Streamer): instance of decrypted - cipher text of .raw which is encrypted qb64, qb2, or sniffable - stream depending on .code + decrypted (Matter | Indexer | Streamer | bytes): When bare is False + returns instance of decrypted cipher text of .raw which is + encrypted qb64, qb2, or sniffable stream depending on .code + hhen Bare is True. Otherwise returns decrypted serialization + plaintext whatever that may be. Keyword Parameters: (see Matter because created Decrypter is Matter subclass) @@ -978,6 +986,8 @@ def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable + bare (bool): False (default) means returns instance holding plaintext + True means returns plaintext itself """ if not cipher: if qb64: # create cipher from qb64 @@ -992,10 +1002,11 @@ def decrypt(self, *, cipher=None, qb64=None, qb2=None, klas=None, return (self._decrypt(cipher=cipher, prikey=self.raw, klas=klas, - transferable=transferable)) + transferable=transferable, + bare=bare)) @staticmethod - def _x25519(cipher, prikey, klas=None, transferable=False): + def _x25519(cipher, prikey, klas=None, transferable=False, bare=False): """ Returns plain text as Salter or Signer instance depending on the cipher code and the embedded encrypted plain text derivation code. @@ -1010,27 +1021,34 @@ def _x25519(cipher, prikey, klas=None, transferable=False): When klas init (such as Signer) supports transferabe parm; True means verfer of returned signer is transferable. False means non-transferable + bare (bool): False (default) means CESR instance holding plaintext + True means plaintext """ + # assumes raw plain text is qb64b or qb64 or sniffable stream + # so it's round trippable pubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) plain = pysodium.crypto_box_seal_open(cipher.raw, pubkey, prikey) # qb64b - # ensure raw plain text is qb64b or qb64 so its derivation code is round tripped - if not klas: - if cipher.code == CiXFixQB64Dex.X25519_Cipher_Salt: - klas = Salter - elif cipher.code == CiXFixQB64Dex.X25519_Cipher_Seed: - klas = Signer + if bare: + return plain + + else: + if not klas: + if cipher.code == CiXFixQB64Dex.X25519_Cipher_Salt: + klas = Salter + elif cipher.code == CiXFixQB64Dex.X25519_Cipher_Seed: + klas = Signer + elif cipher.code in CiXVarStrmDex: + klas = Streamer + else: + raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}" + f" when klas missing.") + + if cipher.code in CiXAllQB64Dex: + return klas(qb64b=plain, transferable=transferable) + elif cipher.code in CiXVarQB2Dex: + return klas(qb2=plain) elif cipher.code in CiXVarStrmDex: - klas = Streamer + return klas(stream=plain) else: - raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}" - f" when klas missing.") - - if cipher.code in CiXAllQB64Dex: - return klas(qb64b=plain, transferable=transferable) - elif cipher.code in CiXVarQB2Dex: - return klas(qb2=plain) - elif cipher.code in CiXVarStrmDex: - return klas(stream=plain) - else: - raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}.") + raise InvalidCodeError(f"Unsupported cipher code = {cipher.code}.") From e74476101420872011401b7d3845cf7720c0b8b1 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 14:59:09 -0600 Subject: [PATCH 14/17] added units tests for bare functionality --- tests/core/test_signing.py | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index 8de9a64f6..25de827c8 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -529,9 +529,11 @@ def test_cipher(): # test .decrypt method needs qb64 prikeyqb64 = Matter(raw=prikey, code=MtrDex.X25519_Private).qb64b assert cipher.decrypt(prikey=prikeyqb64).qb64b == seedqb64b + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == seedqb64b cryptseedqb64 = Matter(raw=cryptseed, code=MtrDex.Ed25519_Seed).qb64b assert cipher.decrypt(seed=cryptseedqb64).qb64b == seedqb64b + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == seedqb64b # wrong but shorter code so instance creation succeeds cipher = Cipher(raw=raw, code=CiXDex.X25519_Cipher_Salt) @@ -557,9 +559,11 @@ def test_cipher(): # test .decrypt method needs qb64 prikeyqb64 = Matter(raw=prikey, code=MtrDex.X25519_Private).qb64b assert cipher.decrypt(prikey=prikeyqb64).qb64b == saltqb64b + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == saltqb64b cryptseedqb64 = Matter(raw=cryptseed, code=MtrDex.Ed25519_Seed).qb64b assert cipher.decrypt(seed=cryptseedqb64).qb64b == saltqb64b + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == saltqb64b with pytest.raises(kering.InvalidCodeError): # bad code cipher = Cipher(raw=raw, code=MtrDex.Ed25519N) @@ -754,11 +758,15 @@ def test_cipher(): result = cipher.decrypt(prikey=prikeyqb64) texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) assert texter.text == plain + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == texter.qb64b # test using seed with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 result = cipher.decrypt(seed=cryptseedqb64) texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) assert texter.text == plain + # test with bare + texter = cipher.decrypt(seed=cryptseedqb64, bare=True) == texter.qb64b # qb64 lead 1 @@ -775,11 +783,15 @@ def test_cipher(): result = cipher.decrypt(prikey=prikeyqb64) texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) assert texter.text == plain + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == texter.qb64b # test using seed with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 result = cipher.decrypt(seed=cryptseedqb64) texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) assert texter.text == plain + # test with bare + texter = cipher.decrypt(seed=cryptseedqb64, bare=True) == texter.qb64b # qb64 lead 2 @@ -796,11 +808,15 @@ def test_cipher(): result = cipher.decrypt(prikey=prikeyqb64) texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) assert texter.text == plain + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == texter.qb64b # test using seed with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 result = cipher.decrypt(seed=cryptseedqb64) texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) assert texter.text == plain + # test with bare + texter = cipher.decrypt(seed=cryptseedqb64, bare=True) == texter.qb64b # test .decrypt method with variable sized qb2 coded ciphers # qb2 lead 0 (always lead 0 when qb2 from texter) @@ -817,11 +833,15 @@ def test_cipher(): result = cipher.decrypt(prikey=prikeyqb64) texter = cipher.decrypt(prikey=prikeyqb64, klas=core.Texter) assert texter.text == plain + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == texter.qb2 # test using seed with pytest.raises(kering.InvalidCodeError): # needs klas when var len qb64 result = cipher.decrypt(seed=cryptseedqb64) texter = cipher.decrypt(seed=cryptseedqb64, klas=core.Texter) assert texter.text == plain + # test with bare + texter = cipher.decrypt(seed=cryptseedqb64, bare=True) == texter.qb2 # sniffable qb64 lead 0 plain = "The quick brown fox jumps over the lazy" @@ -839,17 +859,22 @@ def test_cipher(): assert streamer.stream == strm streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == streamer.stream # test using seed streamer = cipher.decrypt(seed=cryptseedqb64) assert streamer.stream == strm streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == streamer.stream strm = bytearray(strm) counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup texter = core.Texter(qb64b=strm, strip=True) assert texter.text == plain + # sniffable qb64 lead 1 plain = "The quick brown fox jumps over the lazy dog" texter = core.Texter(text=plain) @@ -866,11 +891,15 @@ def test_cipher(): assert streamer.stream == strm streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == streamer.stream # test using seed streamer = cipher.decrypt(seed=cryptseedqb64) assert streamer.stream == strm streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == streamer.stream strm = bytearray(strm) counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup @@ -893,11 +922,15 @@ def test_cipher(): assert streamer.stream == strm streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == streamer.stream # test using seed streamer = cipher.decrypt(seed=cryptseedqb64) assert streamer.stream == strm streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) assert streamer.stream == strm + # test with bare + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == streamer.stream strm = bytearray(strm) counter = core.Counter(qb64b=strm, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup @@ -922,11 +955,15 @@ def test_cipher(): assert streamer.stream == strmb2 streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) assert streamer.stream == strmb2 + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == streamer.stream # test using seed streamer = cipher.decrypt(seed=cryptseedqb64) assert streamer.stream == strmb2 streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) assert streamer.stream == strmb2 + # test with bare + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == streamer.stream strmb2 = bytearray(strmb2) counter = core.Counter(qb2=strmb2, strip=True) assert counter.code == core.CtrDex_2_0.GenericGroup @@ -953,11 +990,15 @@ def test_cipher(): assert streamer.stream == strmb2 streamer = cipher.decrypt(prikey=prikeyqb64, klas=core.Streamer) assert streamer.stream == strmb2 + # test with bare + assert cipher.decrypt(prikey=prikeyqb64, bare=True) == streamer.stream # test using seed streamer = cipher.decrypt(seed=cryptseedqb64) assert streamer.stream == strmb2 streamer = cipher.decrypt(seed=cryptseedqb64, klas=core.Streamer) assert streamer.stream == strmb2 + # test with bare + assert cipher.decrypt(seed=cryptseedqb64, bare=True) == streamer.stream strmb2 = bytearray(strmb2) counter = core.Counter(qb2=strmb2, strip=True) assert counter.code == core.CtrDex_2_0.BigGenericGroup @@ -1107,6 +1148,12 @@ def test_decrypter(): assert designer.verfer.code == MtrDex.Ed25519 assert signer.verfer.transferable + # test bare decryption returns plain not instance + plain = decrypter.decrypt(qb64=seedcipher.qb64b, + transferable=signer.verfer.transferable, + bare=True) + assert plain == seedqb64b + # decrypt seed cipher using cipher designer = decrypter.decrypt(cipher=seedcipher, transferable=signer.verfer.transferable) assert designer.qb64b == seedqb64b @@ -1124,6 +1171,10 @@ def test_decrypter(): assert desalter.qb64b == saltqb64b assert desalter.code == MtrDex.Salt_128 + # test bare decryption returns plain not instance + plain = decrypter.decrypt(qb64=saltcipher.qb64b, bare=True) + assert plain == saltqb64b + # decrypt salt cipher using cipher desalter = decrypter.decrypt(cipher=saltcipher) assert desalter.qb64b == saltqb64b From dbab817d52c5b9c60dd1f4a87f2993432c1f0b58 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 17:08:54 -0600 Subject: [PATCH 15/17] added unit tests of round trip encrypt decrypt --- src/keri/core/signing.py | 9 +++-- tests/core/test_signing.py | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index 254986c9e..d7914523a 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -833,8 +833,9 @@ def encrypt(self, ser=None, prim=None, code=None): ser (str | bytes | bytearray | memoryview): qb64b or qb64 serialization of plain text - prim (Matter | Indexer): CESR primitive instance whose serialization - qb64 or qb2 is to be encrypted based on code + prim (Matter | Indexer | Streamer): CESR primitive instance whose + serialization is qb64 or qb2 or sniffable stream and is to be + encrypted based on code code (str): code of plain text type for resultant encrypted cipher """ if not ser: @@ -857,8 +858,10 @@ def encrypt(self, ser=None, prim=None, code=None): ser = prim.qb64b elif code in CiXVarQB2Dex: ser = prim.qb2 + elif code in CiXVarStrmDex: + ser = prim.stream else: - raise InvalidCodeError(f"Invalide primitive cipher {code=} not " + raise InvalidCodeError(f"Invalid primitive cipher {code=} not " f"qb64 or qb2.") if not code: # assumes default is sniffable stream diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py index 25de827c8..d71c93b12 100644 --- a/tests/core/test_signing.py +++ b/tests/core/test_signing.py @@ -1081,6 +1081,9 @@ def test_encrypter(): uncb = pysodium.crypto_box_seal_open(cipher.raw, encrypter.raw, prikey) assert uncb == saltqb64b + # needs tests of encrypter with prim param instead of ser (see roundtrip) + + """ Done Test """ @@ -1233,13 +1236,92 @@ def test_roundtrip(): assert encrypter.qb64 == 'CAF7Wr3XNq5hArcOuBJzaY6Nd23jgtUVI6KDfb3VngkR' assert encrypter.raw == pubkey + + # Test cipher qb2 (always L0 when qb2) + + plain = "The quick brown fox jumps over the lazy dog" + tin = core.Texter(text=plain) # texter in + # create cipher using Encrypter + cipher = encrypter.encrypt(prim=tin, code=CiXDex.X25519_Cipher_QB2_L0) + assert cipher.code == CiXDex.X25519_Cipher_QB2_L0 + # decrypt cipher using Decrypter + tout = decrypter.decrypt(cipher=cipher, klas=core.Texter) # texter out + assert tout.text == tin.text + + + # sniffable qb64 lead 0 + plain = "The quick brown fox jumps over the lazy" + texter = core.Texter(text=plain) + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + # sniffable streamer in + sin = core.Streamer(stream=counter.qb64b + texter.qb64b) + # create cipher using Encrypter + cipher = encrypter.encrypt(prim=sin, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L0 + # decrypt cipher using Decrypter + # sniffable stream out + sout = decrypter.decrypt(cipher=cipher, klas=core.Streamer) + assert sin.stream == sout.stream + + + # sniffable qb64 lead 1 + plain = "The quick brown fox jumps over the lazy dog" + texter = core.Texter(text=plain) + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + # sniffable streamer in + sin = core.Streamer(stream=counter.qb64b + texter.qb64b) # create cipher using Encrypter + cipher = encrypter.encrypt(prim=sin, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L1 + # decrypt cipher using Decrypter + # sniffable stream out + sout = decrypter.decrypt(cipher=cipher, klas=core.Streamer) + assert sin.stream == sout.stream + # sniffable qb64 lead 2 + plain = "The quick brown fox jumps over the lazy " + texter = core.Texter(text=plain) + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + # sniffable streamer in + sin = core.Streamer(stream=counter.qb64b + texter.qb64b) + # create cipher using Encrypter + cipher = encrypter.encrypt(prim=sin, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L2 # decrypt cipher using Decrypter + # sniffable stream out + sout = decrypter.decrypt(cipher=cipher, klas=core.Streamer) + assert sin.stream == sout.stream + # sniffable qb2 lead 0 + plain = "The quick brown fox jumps over the lazy" + texter = core.Texter(text=plain) + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + # sniffable streamer in + sin = core.Streamer(stream=counter.qb2 + texter.qb2) + # create cipher using Encrypter + cipher = encrypter.encrypt(prim=sin, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_L0 + # decrypt cipher using Decrypter + # sniffable stream out + sout = decrypter.decrypt(cipher=cipher, klas=core.Streamer) + assert sin.stream == sout.stream + # sniffable qb2 lead 0 Big + plain = "The quick brown fox jumps over the lazy" * 324 + texter = core.Texter(text=plain) + counter = core.Counter(core.Codens.GenericGroup, count=texter.size) + assert counter.code == core.CtrDex_2_0.BigGenericGroup + # sniffable streamer in + sin = core.Streamer(stream=counter.qb2 + texter.qb2) + # create cipher using Encrypter + cipher = encrypter.encrypt(prim=sin, code=CiXDex.X25519_Cipher_L0) + assert cipher.code == CiXDex.X25519_Cipher_Big_L0 + # decrypt cipher using Decrypter + # sniffable stream out + sout = decrypter.decrypt(cipher=cipher, klas=core.Streamer) + assert sin.stream == sout.stream """End Test""" From ac5e4df0b43251a5b094539e87a71d46295d4414 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 17:24:48 -0600 Subject: [PATCH 16/17] added support for prim to be Streamer instance --- src/keri/core/signing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keri/core/signing.py b/src/keri/core/signing.py index d7914523a..7a4faa63e 100644 --- a/src/keri/core/signing.py +++ b/src/keri/core/signing.py @@ -823,7 +823,7 @@ def verifySeed(self, seed): pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey) return (pubkey == self.raw) - def encrypt(self, ser=None, prim=None, code=None): + def encrypt(self, *, ser=None, prim=None, code=None): """ Returns: Cipher instance of cipher text encryption of plain text serialization @@ -831,8 +831,8 @@ def encrypt(self, ser=None, prim=None, code=None): Parameters: - ser (str | bytes | bytearray | memoryview): qb64b or qb64 - serialization of plain text + ser (str | bytes | bytearray | memoryview): qb64b or qb64 or sniffable + stream serialization of plain text prim (Matter | Indexer | Streamer): CESR primitive instance whose serialization is qb64 or qb2 or sniffable stream and is to be encrypted based on code @@ -872,9 +872,9 @@ def encrypt(self, ser=None, prim=None, code=None): if not isinstance(ser, bytes): ser = bytes(ser) # convert bytearray and memoryview to bytes - # encrypting fully qualified qb64 version of cesr primitive as plain + # encrypting cesr primitive qb64 or qb2 or cesr stream as plain # text with proper cipher code ensures primitive round trip through eventual - # decryption. Likewise for sniffable stream with sniffible cipher code. + # decryption. return (self._encrypt(ser=ser, pubkey=self.raw, code=code)) @staticmethod From cb4bce5c553e81626b32ddfc5f75bec8b4cebd82 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 5 Aug 2024 17:29:16 -0600 Subject: [PATCH 17/17] updated comments on streamer methods --- src/keri/core/streaming.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/keri/core/streaming.py b/src/keri/core/streaming.py index be88485c9..f00e570cc 100644 --- a/src/keri/core/streaming.py +++ b/src/keri/core/streaming.py @@ -362,9 +362,12 @@ def __init__(self, stream, verify=False): @property def _verify(self): """Returns True if sniffable stream, False otherwise - Returns: - sniffable (bool): True when .stream is sniffable. + Returns: + sniffable (bool): True when .stream is sniffable. False otherwise. + Only works for ver 2 CESR because need for all count codes to be + pipelineable in order to simply parse stream + """ return False @@ -384,6 +387,9 @@ def text(self): Returns: stream (bytes): expanded text qb64 version of stream + Only works for ver 2 CESR because need for all count codes to be + pipelineable in order to simply parse and expand stream + """ return self._stream @@ -395,6 +401,9 @@ def binary(self): Returns: stream (bytes): compacted binary qb2 version of stream + Only works for ver 2 CESR because need for all count codes to be + pipelineable in order to simply parse and compact stream + """ return self._stream