From 696aca4c9ced39484a9a0cf3a5cf108874f1cdff Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Mon, 2 Feb 2026 18:13:28 -0700 Subject: [PATCH] added decodeQVK method to Memoer --- src/hio/core/memo/memoing.py | 43 +++++++++++++++++++++++++++++++++ tests/core/memo/test_memoing.py | 7 ++++++ 2 files changed, 50 insertions(+) diff --git a/src/hio/core/memo/memoing.py b/src/hio/core/memo/memoing.py index 91668af..a912978 100644 --- a/src/hio/core/memo/memoing.py +++ b/src/hio/core/memo/memoing.py @@ -550,6 +550,49 @@ def _encodeQVK(cls, raw, code='B'): return qb64 # qualified base64 verkey + @classmethod + def _decodeQVK(cls, qb64): + """Utility method for use with signed headers that decodes qualified + base64 verkey to raw domain bytes from CESR compatible text code + + Parameters: + qb64 (str): qualified base64 verkey to be decoded with code + code (str): code for type of raw verkey CESR compatible + Ed25519N: str = 'B' # Ed25519 verkey non-transferable, basic derivation. + + Returns: + tuple(raw, code) where: + raw (bytes): verkey suitable for crypto operations + code (str): CESR compatible code from qb64 + """ + cz = 1 # only support qb64 length 44 + code = qb64[:cz] + if code not in ('B'): + raise hioing.MemoerError(f"Invalid qvk {code=}") + + qz = len(qb64) # text size + if qz != 44: + raise hioing.MemoerError(f"Invalid qvk text size {qz=} not 44") + + cz = len(code) + pz = cz % 4 # net pad size given cz + if cz != pz != 1: # special case here for now we only accept cz=1 + raise hioing.MemoerError(f"Invalid {cz=} not equal {pz=} not equal 1") + + base = pz * b'A' + qb64[cz:].encode() # strip code from b64 and prepad pz 'A's + paw = decodeB64(base) # now should have pz leading sextexts of zeros + raw = paw[pz:] # remove prepad midpad bytes to invert back to raw + # ensure midpad bytes are zero + pi = int.from_bytes(paw[:pz], "big") + if pi != 0: + raise hioing.MemoerError(f"Nonzero midpad bytes=0x{pi:0{(pz)*2}x}.") + + if len(raw) != ((qz - cz) * 3 // 4): # exact lengths + raise hioing.MemoerError(f"Improperly qualified material = {qss}") + + return raw, code + + @classmethod def _encodeQSS(cls, raw, code='A'): """Utility method for use with signed headers that encodes raw sigseed as diff --git a/tests/core/memo/test_memoing.py b/tests/core/memo/test_memoing.py index 37d9659..036acfe 100644 --- a/tests/core/memo/test_memoing.py +++ b/tests/core/memo/test_memoing.py @@ -149,6 +149,13 @@ def test_memoer_class(): _, _, oz, _, _, _ = Memoer.Sizes[SGDex.Signed] # cz mz oz nz sz hz assert len(oid) == 44 == oz + qvk = Memoer._encodeQVK(raw=verkey) + assert qvk == 'BG-R9L4kTXULe33Tqidn0c-W-x6xU4lIXCdhZQYrsih2' + raw, code = Memoer._decodeQVK(oid) + assert raw == verkey + assert code == 'B' + assert len(qvk) == 44 + sigseed = (b"\x9bF\n\xf1\xc2L\xeaBC:\xf7\xe9\xd71\xbc\xd2{\x7f\x81\xae5\x9c\xca\xf9\xdb\xac@`'\x0e\xa4\x10") qss = Memoer._encodeQSS(raw=sigseed) assert qss == 'AJtGCvHCTOpCQzr36dcxvNJ7f4GuNZzK-dusQGAnDqQQ'