diff --git a/src/seedsigner/models/__init__.py b/src/seedsigner/models/__init__.py index be9de694..c81594b6 100644 --- a/src/seedsigner/models/__init__.py +++ b/src/seedsigner/models/__init__.py @@ -1,3 +1,4 @@ +from .qr_type import * from .decode_qr import * from .encode_qr import * from .psbt_parser import * diff --git a/src/seedsigner/models/decode_qr.py b/src/seedsigner/models/decode_qr.py index 17470a50..3e1669e1 100644 --- a/src/seedsigner/models/decode_qr.py +++ b/src/seedsigner/models/decode_qr.py @@ -1,12 +1,13 @@ from pyzbar import pyzbar from pyzbar.pyzbar import ZBarSymbol -import re from enum import IntEnum +import re import base64 from embit import bip39, psbt from binascii import a2b_base64, b2a_base64 from seedsigner.helpers.ur2.ur_decoder import URDecoder from seedsigner.helpers.bcur import (cbor_decode, bc32decode) +from seedsigner.models.qr_type import QRType ### ### DecodeQR Class @@ -15,15 +16,19 @@ from seedsigner.helpers.bcur import (cbor_decode, bc32decode) class DecodeQR: - def __init__(self, qr_type=None): + def __init__(self, **kwargs): self.complete = False - self.qr_type = qr_type + self.qr_type = None self.ur_decoder = URDecoder() # UR2 decoder self.specter_qr = SpecterDecodePSBTQR() # Specter Desktop PSBT QR base64 decoder self.legacy_ur = LegacyURDecodeQR() # UR Legacy decoder self.base64_qr = Base64DecodeQR() # Single Segments Base64 self.seedqr = SeedQR() + for key, value in kwargs.items(): + if key == "qr_type": + self.qr_type = value + def addImage(self, image): qr_str = DecodeQR.QR2Str(image) if not qr_str: @@ -402,21 +407,6 @@ class SeedQR: return self.seed_phrase return None - -### -### QRType Class IntEum -### Purpose: used in DecodeQR to communicate qr encoding type -### - -class QRType(IntEnum): - PSBTBASE64 = 1 - PSBTSPECTER = 2 - PSBTURLEGACY = 3 - PSBTUR2 = 5 - SEEDSSQR = 6 - SEEDUR2 = 7 - INVALID = 100 - ### ### DecodeQRStatus Class IntEum ### Purpose: used in DecodeQR to communicate status of adding qr frame/segment diff --git a/src/seedsigner/models/encode_qr.py b/src/seedsigner/models/encode_qr.py index 09b1472e..531f7ce9 100644 --- a/src/seedsigner/models/encode_qr.py +++ b/src/seedsigner/models/encode_qr.py @@ -1,12 +1,13 @@ from enum import IntEnum -from embit import psbt, bip39 -from binascii import b2a_base64 +from embit import psbt, bip39, bip32 +from embit.networks import NETWORKS +from binascii import b2a_base64, hexlify from seedsigner.helpers.ur2.ur_encoder import UREncoder from seedsigner.helpers.ur2.cbor_lite import CBOREncoder from seedsigner.helpers.ur2.ur import UR from seedsigner.helpers.bcur import (bc32encode, cbor_encode, bcur_encode) from seedsigner.helpers.qr import QR -from seedsigner.models.decode_qr import QRType +from seedsigner.models.qr_type import QRType ### ### EncodeQR Class @@ -15,32 +16,52 @@ from seedsigner.models.decode_qr import QRType class EncodeQR: - def __init__(self, p, qr_type=None, qr_density=None): - if qr_type == None and self.psbt != None: - self.qr_type = QRType.PSBTSPECTER - else: - self.qr_type = qr_type - - if self.qr_type in (QRType.PSBTSPECTER, QRType.PSBTUR2): - self.psbt = p - else: - self.psbt = None - - if qr_density == None: - self.qr_density = EncodeQRDensity.MEDIUM - else: - self.qr_density = qr_density - + def __init__(self, **kwargs): + self.psbt = None + self.seed_phrase = None + self.passphrase = None + self.derivation = None + self.network = None + self.policy = None + self.qr_type = None + self.qr_density = None self.qr = QR() + for key, value in kwargs.items(): + if key == "psbt": + self.psbt = value + elif key == "seed_phrase": + self.seed_phrase = value + elif key == "passphrase": + self.passphrase = value + elif key == "derivation": + self.derivation = value + elif key == "network": + self.network = value + elif key == "policy": + self.policy = value + elif key == "qr_type": + self.qr_type = value + elif key == "qr_density": + self.qr_density = value + + if self.qr_type == None: + raise Exception('Encoder Type Required') + + if self.qr_density == None: + self.qr_density = EncodeQRDensity.MEDIUM + self.encoder = None - if qr_type == QRType.PSBTSPECTER: + if self.qr_type == QRType.PSBTSPECTER: self.encoder = SpecterEncodePSBTQR(self.psbt, self.qr_density) - elif qr_type == QRType.PSBTUR2: + elif self.qr_type == QRType.PSBTUR2: self.encoder = UREncodePSBTQR(self.psbt, self.qr_density) - elif qr_type == QRType.SEEDSSQR: - seed_phrase = p - self.encoder = SeedSSQR(seed_phrase) + elif self.qr_type == QRType.SEEDSSQR: + self.encoder = SeedSSQR(self.seed_phrase) + elif self.qr_type == QRType.XPUBQR: + self.encoder = XPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy) + elif self.qr_type == QRType.SPECTERXPUBQR: + self.encoder = SpecterXPubQR(self.seed_phrase, self.passphrase, self.derivation, self.network, self.policy, self.qr_density) else: raise Exception('Encoder Type not Supported') @@ -180,6 +201,109 @@ class SeedSSQR: def isComplete(self): return True +class XPubQR: + + def __init__(self, seed_phrase, passphrase, derivation, network, policy): + self.seed_phrase = seed_phrase + self.passphrase = passphrase + self.derivation = derivation + self.parts = [] + self.part_num_sent = 0 + self.sent_complete = False + + self.network = network + self.seed = bip39.mnemonic_to_seed((" ".join(self.seed_phrase)).strip(), self.passphrase) + self.root = bip32.HDKey.from_seed(self.seed, version=NETWORKS[self.network]["xprv"]) + self.fingerprint = self.root.child(0).fingerprint + self.bip48_xprv = self.root.derive(self.derivation) + self.bip48_xpub = self.bip48_xprv.to_public() + + xpub_base58 = "" + + if policy == "PKWPKH": + self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["zpub"]) + elif policy == "PKWSH": + self.xpub_base58 = self.bip48_xpub.to_base58(NETWORKS[self.network]["Zpub"]) + else: + raise Exception('Policy Type not Supported') + + self.xpubstring = "[%s%s]%s" % (hexlify(self.fingerprint).decode('utf-8'),self.derivation[1:],self.xpub_base58) + + if isinstance(self, XPubQR): + self.__createParts() + + def __createParts(self): + self.parts = [] + self.parts.append(self.xpubstring) + + def nextPart(self) -> str: + if len(self.parts) > 0: + self.sent_complete = True + return self.parts[0] + + def seqLen(self): + return len(self.parts) + + def isComplete(self): + return self.sent_complete + +class SpecterXPubQR(XPubQR): + + def __init__(self, seed_phrase, passphrase, derivation, network, policy, qr_density): + self.qr_max_fragement_size = 65 + if qr_density == EncodeQRDensity.LOW: + self.qr_max_fragement_size = 40 + elif qr_density == EncodeQRDensity.MEDIUM: + self.qr_max_fragement_size = 65 + elif qr_density == EncodeQRDensity.HIGH: + self.qr_max_fragement_size = 90 + + XPubQR.__init__(self, seed_phrase, passphrase, derivation, network, policy) + self.__createParts() + + def __createParts(self): + + self.parts = [] + + start = 0 + stop = self.qr_max_fragement_size + qr_cnt = ((len(self.xpubstring)-1) // self.qr_max_fragement_size) + 1 + + if qr_cnt == 1: + self.parts.append(self.xpubstring[start:stop]) + + cnt = 0 + while cnt < qr_cnt and qr_cnt != 1: + part = "p" + str(cnt+1) + "of" + str(qr_cnt) + " " + self.xpubstring[start:stop] + self.parts.append(part) + + start = start + self.qr_max_fragement_size + stop = stop + self.qr_max_fragement_size + if stop > len(self.xpubstring): + stop = len(self.xpubstring) + cnt += 1 + + def nextPart(self) -> str: + # if part num sent is gt number of parts, start at 0 + if self.part_num_sent > (len(self.parts) - 1): + self.part_num_sent = 0 + + part = self.parts[self.part_num_sent] + + # when parts sent eq num of parts in list + if self.part_num_sent == (len(self.parts) - 1): + self.sent_complete = True + + # increment to next part + self.part_num_sent += 1 + + return part + + def seqLen(self): + return len(self.parts) + + def isComplete(self): + return self.sent_complete class EncodeQRDensity(IntEnum): LOW = 1 diff --git a/src/seedsigner/models/qr_type.py b/src/seedsigner/models/qr_type.py new file mode 100644 index 00000000..502d6c24 --- /dev/null +++ b/src/seedsigner/models/qr_type.py @@ -0,0 +1,17 @@ +from enum import IntEnum + +### +### QRType Class IntEum +### Purpose: used with DecodeQR and EncodeQR to communicate qr encoding type +### + +class QRType(IntEnum): + PSBTBASE64 = 1 + PSBTSPECTER = 2 + PSBTURLEGACY = 3 + PSBTUR2 = 5 + SEEDSSQR = 6 + SEEDUR2 = 7 + XPUBQR = 8 + SPECTERXPUBQR = 9 + INVALID = 100 \ No newline at end of file diff --git a/tests/test_encodepsbtqr.py b/tests/test_encodepsbtqr.py index a9bddb46..b38239d1 100644 --- a/tests/test_encodepsbtqr.py +++ b/tests/test_encodepsbtqr.py @@ -11,7 +11,7 @@ def test_ur_qr_encode(): tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(tx, QRType.PSBTUR2) + e = EncodeQR(psbt=tx, qr_type=QRType.PSBTUR2) cnt = 0 while cnt <= 10: @@ -26,7 +26,7 @@ def test_specter_qr_encode(): tx = psbt.PSBT.parse(a2b_base64(base64_psbt)) - e = EncodeQR(tx, QRType.PSBTSPECTER) + e = EncodeQR(psbt=tx, qr_type=QRType.PSBTSPECTER) cnt = 0 while cnt <= 10: @@ -61,11 +61,29 @@ def test_seedsigner_qr(): mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" - e = EncodeQR(mnemonic.split(" "), QRType.SEEDSSQR) + e = EncodeQR(seed_phrase=mnemonic.split(" "), qr_type=QRType.SEEDSSQR) print(e.nextPart()) assert e.nextPart() == "121802020768124106400009195602431595117715840445" +def test_xpub_qr(): + + mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.XPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h") + + assert e.nextPart() == "[c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6CzuiF6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8PbuU6smtv8HLcTEAv" + +def test_specter_xpub_qr(): + + mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash" + + e = EncodeQR(seed_phrase=mnemonic.split(" "), passphrase="pass", qr_type=QRType.SPECTERXPUBQR, network="test", policy="PKWPKH", derivation="m/48h/1h/0h/2h", qr_density=EncodeQRDensity.LOW) + + assert e.nextPart() == "p1of4 [c49122a5/48h/1h/0h/2h]vpub5adb6xr5X1yqx" + assert e.nextPart() == "p2of4 2v7qoo5uR32BBCQsj9tRe9zbMRsH8X6tYGE6Czui" + assert e.nextPart() == "p3of4 F6k3njCyHrU5JWPMsoPwNZd9Bk9Kaqy29izB8Pbu" + assert e.nextPart() == "p4of4 U6smtv8HLcTEAv"