diff --git a/REFERENCE/Screenshot 2026-02-04 at 13.11.17.png b/REFERENCE/Screenshot 2026-02-04 at 13.11.17.png deleted file mode 100644 index 8ca4636..0000000 Binary files a/REFERENCE/Screenshot 2026-02-04 at 13.11.17.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-05 at 01.11.32.png b/REFERENCE/Screenshot 2026-02-05 at 01.11.32.png deleted file mode 100644 index 6f03454..0000000 Binary files a/REFERENCE/Screenshot 2026-02-05 at 01.11.32.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-05 at 01.11.43.png b/REFERENCE/Screenshot 2026-02-05 at 01.11.43.png deleted file mode 100644 index 75e4cfb..0000000 Binary files a/REFERENCE/Screenshot 2026-02-05 at 01.11.43.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-05 at 01.11.52.png b/REFERENCE/Screenshot 2026-02-05 at 01.11.52.png deleted file mode 100644 index a1a591a..0000000 Binary files a/REFERENCE/Screenshot 2026-02-05 at 01.11.52.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-06 at 01.42.45.png b/REFERENCE/Screenshot 2026-02-06 at 01.42.45.png deleted file mode 100644 index 911b92a..0000000 Binary files a/REFERENCE/Screenshot 2026-02-06 at 01.42.45.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-06 at 23.49.37.png b/REFERENCE/Screenshot 2026-02-06 at 23.49.37.png deleted file mode 100644 index bd36144..0000000 Binary files a/REFERENCE/Screenshot 2026-02-06 at 23.49.37.png and /dev/null differ diff --git a/REFERENCE/Screenshot 2026-02-07 at 02.26.22.png b/REFERENCE/Screenshot 2026-02-07 at 02.26.22.png deleted file mode 100644 index 78bd579..0000000 Binary files a/REFERENCE/Screenshot 2026-02-07 at 02.26.22.png and /dev/null differ diff --git a/REFERENCE/baseconv(2).py b/REFERENCE/baseconv(2).py deleted file mode 100644 index 38faeeb..0000000 --- a/REFERENCE/baseconv(2).py +++ /dev/null @@ -1,192 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021-2024 Krux contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -from binascii import a2b_base64, b2a_base64 - - -def base_decode(v, base): - """Abstraction to decode the str data in v as base; returns bytes""" - if not isinstance(v, str): - raise TypeError("Invalid value, expected str") - - if v == "": - return b"" - - # Base32 and Base43 are implemented custom in MaixPy on k210, else in python for simulator - # Base58 is implemented in pure_python_base_decode() below - # Base64 is a special case: We just use binascii's implementation without - # performing bitcoin-specific padding logic - if base == 32: - import base32 - - return base32.decode(v) - if base == 43: - import base43 - - return base43.decode(v) - if base == 58: - return pure_python_base_decode(v, 58) - if base == 64: - return a2b_base64(v) - - raise ValueError("not supported base: {}".format(base)) - - -def base_encode(v, base): - """Abstraction to encode the bytes data in v as base; returns str""" - if not isinstance(v, bytes): - raise TypeError("Invalid value, expected bytes") - - if v == b"": - return "" - - # Base32 and Base43 are implemented custom in MaixPy on k210, else in python for simulator - # Base58 is implemented in pure_python_base_encode() below - # Base64 is a special case: We just use binascii's implementation without - # performing bitcoin-specific padding logic. b2a_base64 always adds a \n - # char at the end which we strip before returning - if base == 32: - import base32 - - return base32.encode(v, False) - if base == 43: - import base43 - - return base43.encode(v, False) - if base == 58: - return pure_python_base_encode(v, 58) - if base == 64: - return b2a_base64(v).rstrip().decode() - - raise ValueError("not supported base: {}".format(base)) - - -def hint_encodings(str_data): - """NON-VERIFIED encoding hints of what input string might be, returns list""" - - if not isinstance(str_data, str): - raise TypeError("hint_encodings() expected str") - - encodings = [] - - # get min and max characters (sorted by ordinal value), - # check most restrictive encodings first - # is not strict -- does not try to decode -- assumptions are made - - min_chr = min(str_data) - max_chr = max(str_data) - - # might it be hex - if len(str_data) % 2 == 0 and "0" <= min_chr: - if max_chr <= "F": - encodings.append("HEX") - elif max_chr <= "f": - encodings.append("hex") - - # might it be base32 - if "2" <= min_chr and max_chr <= "Z": - encodings.append(32) - - # might it be base43 - if "$" <= min_chr and max_chr <= "Z": - encodings.append(43) - - # might it be base58? currently unused - # if "1" <= min_chr and max_chr <= "z": - # encodings.append(58) - - # might it be base64 - if "+" <= min_chr and max_chr <= "z": - encodings.append(64) - - # might it be ascii - if ord(max_chr) <= 127: - encodings.append("ascii") - - # might it be latin-1 or utf8 - if 128 <= ord(max_chr) <= 255: - encodings.append("latin-1") - else: - encodings.append("utf8") - - return encodings - - -# pure-python encoder/decoder for base43 and base58 below -B43CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:" -B58CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - - -def pure_python_base_decode(v, base): - """decode str v from base encoding; returns bytes""" - chars = B58CHARS if base == 58 else B43CHARS - long_value = 0 - power_of_base = 1 - for char in reversed(v): - digit = chars.find(char) - if digit == -1: - raise ValueError("forbidden character {} for base {}".format(char, base)) - long_value += digit * power_of_base - power_of_base *= base - result = bytearray() - while long_value >= 256: - div, mod = divmod(long_value, 256) - result.append(mod) - long_value = div - if long_value > 0: - result.append(long_value) - n_pad = 0 - for char in v: - if char == chars[0]: - n_pad += 1 - else: - break - if n_pad > 0: - result.extend(b"\x00" * n_pad) - return bytes(reversed(result)) - - -def pure_python_base_encode(v, base): - """decode bytes v from base encoding; returns str""" - chars = B58CHARS if base == 58 else B43CHARS - long_value = 0 - power_of_base = 1 - for char in reversed(v): - long_value += power_of_base * char - power_of_base <<= 8 - result = bytearray() - while long_value >= base: - div, mod = divmod(long_value, base) - result.extend(chars[mod].encode()) - long_value = div - if long_value > 0: - result.extend(chars[long_value].encode()) - # Bitcoin does a little leading-zero-compression: - # leading 0-bytes in the input become leading-1s - n_pad = 0 - for char in v: - if char == 0x00: - n_pad += 1 - else: - break - if n_pad > 0: - result.extend((chars[0] * n_pad).encode()) - return bytes(reversed(result)).decode() diff --git a/REFERENCE/baseconv.py b/REFERENCE/baseconv.py deleted file mode 100644 index 804ae10..0000000 --- a/REFERENCE/baseconv.py +++ /dev/null @@ -1,65 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021-2025 Krux contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import sys -import base64 -from krux import baseconv - -class base43: - - def encode(data, add_padding=False): - """Encodes data to Base43.""" - return baseconv.pure_python_base_encode(data, 43) - - def decode(encoded_str): - """Decodes a Base43 string.""" - return baseconv.pure_python_base_decode(encoded_str, 43) - - -class base32: - """ - Mock for the base32 module. - """ - def encode(data, add_padding=False): - """Encodes data to Base32.""" - encoded = base64.b32encode(data).decode('utf-8') - if not add_padding: - encoded = encoded.rstrip('=') - return encoded - - def decode(encoded_str): - """Decodes a Base32 string.""" - try: - len_pad = (8 - len(encoded_str) % 8) % 8 - decoded = base64.b32decode(encoded_str + ("=" * len_pad)) - except ValueError as e: - raise ValueError("Invalid Base32 string: %s" % e) - - return decoded - - - -if "base32" not in sys.modules: - sys.modules["base32"] = base32 - -if "base43" not in sys.modules: - sys.modules["base43"] = base43 diff --git a/REFERENCE/bun-CompactseedQR-implement.md.rtf b/REFERENCE/bun-CompactseedQR-implement.md.rtf deleted file mode 100644 index 2bce61d..0000000 --- a/REFERENCE/bun-CompactseedQR-implement.md.rtf +++ /dev/null @@ -1,692 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2867 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;\f1\froman\fcharset0 Times-Bold;\f2\froman\fcharset0 TimesNewRomanPSMT; -\f3\fmodern\fcharset0 Courier-Bold;\f4\fnil\fcharset0 HelveticaNeue;\f5\fmodern\fcharset0 Courier; -\f6\fmodern\fcharset0 Courier-Oblique;\f7\fnil\fcharset0 Menlo-Italic;\f8\fnil\fcharset0 AppleColorEmoji; -\f9\fnil\fcharset0 Menlo-Regular;} -{\colortbl;\red255\green255\blue255;\red255\green255\blue255;\red0\green0\blue0;\red185\green188\blue186; -\red162\green127\blue173;\red166\green178\blue85;\red111\green144\blue176;\red212\green128\blue77;\red132\green134\blue132; -\red0\green0\blue0;\red0\green0\blue233;} -{\*\expandedcolortbl;;\cssrgb\c100000\c100000\c100000;\cssrgb\c0\c0\c0\c84706;\cssrgb\c77255\c78431\c77647; -\cssrgb\c69804\c58039\c73333;\cssrgb\c70980\c74118\c40784;\cssrgb\c50588\c63529\c74510;\cssrgb\c87059\c57647\c37255;\cssrgb\c58824\c59608\c58824; -\cssrgb\c0\c0\c0;\cssrgb\c0\c0\c93333;} -{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}}{\leveltext\leveltemplateid1\'01\'00;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid1}} -{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}} -\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 -\deftab720 -\pard\pardeftab720\sa240\partightenfactor0 - -\f0\fs24 \cf2 \expnd0\expndtw0\kerning0 -\outl0\strokewidth0 \strokec2 You're right to clarify! The test code I provided -\f1\b only covers reading/decoding CompactSeedQR -\f0\b0 (direction 2). Let me provide a -\f1\b complete bidirectional test -\f0\b0 that covers both:\ -\pard\tx220\tx720\pardeftab720\li720\fi-720\sa240\partightenfactor0 -\ls1\ilvl0 -\f1\b \cf2 \kerning1\expnd0\expndtw0 \outl0\strokewidth0 {\listtext 1 }\expnd0\expndtw0\kerning0 -\outl0\strokewidth0 \strokec2 Encoding -\f0\b0 : Mnemonic -\f2 \uc0\u8594 -\f0 CompactSeedQR (binary data that goes into QR code)\ -\ls1\ilvl0 -\f1\b \kerning1\expnd0\expndtw0 \outl0\strokewidth0 {\listtext 2 }\expnd0\expndtw0\kerning0 -\outl0\strokewidth0 \strokec2 Decoding -\f0\b0 : CompactSeedQR -\f2 \uc0\u8594 -\f0 Mnemonic (restore from QR)\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Complete Bidirectional Test ( -\f3\fs39 compact-seedqr-complete.ts -\f1\fs36 )\ -\pard\pardeftab720\qc\partightenfactor0 - -\f4\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f5\fs26 \cf2 \strokec2 bash\ -\pard\pardeftab720\partightenfactor0 -\cf4 \strokec4 bun add bip39 qrcode\ -\pard\pardeftab720\qc\partightenfactor0 - -\f4\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f5\fs26 \cf2 \strokec2 typescript\ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 import -\f5\b0 \cf4 \strokec4 * -\f3\b \cf5 \strokec5 as -\f5\b0 \cf4 \strokec4 bip39 -\f3\b \cf5 \strokec5 from -\f5\b0 \cf4 \strokec4 \cf6 \strokec6 "bip39"\cf4 \strokec4 ;\ - -\f3\b \cf5 \strokec5 import -\f5\b0 \cf4 \strokec4 QRCode -\f3\b \cf5 \strokec5 from -\f5\b0 \cf4 \strokec4 \cf6 \strokec6 "qrcode"\cf4 \strokec4 ;\ -\ - -\f3\b \cf5 \strokec5 interface -\f5\b0 \cf4 \strokec4 \cf7 \strokec7 CompactSeedQRTestVector\cf4 \strokec4 \{\ - name: \cf8 \strokec8 string\cf4 \strokec4 ;\ - mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ;\ - hexEntropy: \cf8 \strokec8 string\cf4 \strokec4 ;\ - wordCount: \cf8 \strokec8 12\cf4 \strokec4 | \cf8 \strokec8 24\cf4 \strokec4 ;\ -\}\ -\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 TEST_VECTORS: CompactSeedQRTestVector[] = [\ - \{\ - name: \cf6 \strokec6 "Test Vector 1: 24-word"\cf4 \strokec4 ,\ - mnemonic: \cf6 \strokec6 "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire"\cf4 \strokec4 ,\ - hexEntropy: \cf6 \strokec6 "0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a"\cf4 \strokec4 ,\ - wordCount: \cf8 \strokec8 24\cf4 \strokec4 \ - \},\ - \{\ - name: \cf6 \strokec6 "Test Vector 4: 12-word"\cf4 \strokec4 ,\ - mnemonic: \cf6 \strokec6 "forum undo fragile fade shy sign arrest garment culture tube off merit"\cf4 \strokec4 ,\ - hexEntropy: \cf6 \strokec6 "5bbd9d71a8ec799083laff359d456545"\cf4 \strokec4 ,\ - wordCount: \cf8 \strokec8 12\cf4 \strokec4 \ - \},\ - \{\ - name: \cf6 \strokec6 "Test Vector 6: 12-word (with null byte)"\cf4 \strokec4 ,\ - mnemonic: \cf6 \strokec6 "approve fruit lens brass ring actual stool coin doll boss strong rate"\cf4 \strokec4 ,\ - hexEntropy: \cf6 \strokec6 "0acbba008d9ba005f5996b40a3475cd9"\cf4 \strokec4 ,\ - wordCount: \cf8 \strokec8 12\cf4 \strokec4 \ - \}\ -];\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // DIRECTION 1: ENCODE - Mnemonic -\f7 \uc0\u8594 -\f6 CompactSeedQR (Binary for QR Code) -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ -\ - -\f6\i \cf9 \strokec9 /**\ - * Convert mnemonic to CompactSeedQR format (raw entropy bytes)\ - * This is what you encode into the QR code as BINARY data\ - */ -\f5\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 encodeToCompactSeedQR(mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ): Buffer \{\ - -\f6\i \cf9 \strokec9 // Validate mnemonic -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 if -\f5\b0 \cf4 \strokec4 (!bip39.validateMnemonic(mnemonic)) \{\ - -\f3\b \cf5 \strokec5 throw -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 new -\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\cf6 \strokec6 "Invalid mnemonic"\cf4 \strokec4 );\ - \}\ -\ - -\f6\i \cf9 \strokec9 // Extract entropy (WITHOUT checksum) -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // bip39.mnemonicToEntropy returns hex string -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropyHex = bip39.mnemonicToEntropy(mnemonic);\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropy = Buffer.from(entropyHex, \cf6 \strokec6 "hex"\cf4 \strokec4 );\ -\ - -\f6\i \cf9 \strokec9 // Validate length -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 if -\f5\b0 \cf4 \strokec4 (entropy.length !== \cf8 \strokec8 16\cf4 \strokec4 && entropy.length !== \cf8 \strokec8 32\cf4 \strokec4 ) \{\ - -\f3\b \cf5 \strokec5 throw -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 new -\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\cf6 \strokec6 `Invalid entropy length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \}\ -\ - -\f3\b \cf5 \strokec5 return -\f5\b0 \cf4 \strokec4 entropy;\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 /**\ - * Generate a QR code from mnemonic (as PNG data URL)\ - */ -\f5\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 async -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 generateCompactSeedQR(mnemonic: \cf8 \strokec8 string\cf4 \strokec4 ): \cf8 \strokec8 Promise\cf4 \strokec4 <\cf8 \strokec8 string\cf4 \strokec4 > \{\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropy = encodeToCompactSeedQR(mnemonic);\ - \ - -\f6\i \cf9 \strokec9 // Generate QR code with BINARY data -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // Important: Use 'byte' mode for raw binary data -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 qrDataURL = -\f3\b \cf5 \strokec5 await -\f5\b0 \cf4 \strokec4 QRCode.toDataURL([\{ data: entropy, mode: \cf6 \strokec6 'byte'\cf4 \strokec4 \}], \{\ - errorCorrectionLevel: \cf6 \strokec6 'L'\cf4 \strokec4 , -\f6\i \cf9 \strokec9 // SeedSigner uses Low error correction -\f5\i0 \cf4 \strokec4 \ - type: \cf6 \strokec6 'image/png'\cf4 \strokec4 ,\ - width: \cf8 \strokec8 300\cf4 \strokec4 \ - \});\ -\ - -\f3\b \cf5 \strokec5 return -\f5\b0 \cf4 \strokec4 qrDataURL;\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // DIRECTION 2: DECODE - CompactSeedQR -\f7 \uc0\u8594 -\f6 Mnemonic (Restore from QR) -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ -\ - -\f6\i \cf9 \strokec9 /**\ - * Parse CompactSeedQR from raw bytes (what QR scanner gives you)\ - */ -\f5\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 decodeCompactSeedQR(data: Buffer | Uint8Array): \cf8 \strokec8 string\cf4 \strokec4 \{\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropy = Buffer.from(data);\ -\ - -\f6\i \cf9 \strokec9 // Validate length -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 if -\f5\b0 \cf4 \strokec4 (entropy.length !== \cf8 \strokec8 16\cf4 \strokec4 && entropy.length !== \cf8 \strokec8 32\cf4 \strokec4 ) \{\ - -\f3\b \cf5 \strokec5 throw -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 new -\f5\b0 \cf4 \strokec4 \cf7 \strokec7 Error\cf4 \strokec4 (\ - \cf6 \strokec6 `Invalid CompactSeedQR length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 bytes. Must be 16 (12-word) or 32 (24-word).`\cf4 \strokec4 \ - );\ - \}\ -\ - -\f6\i \cf9 \strokec9 // Convert entropy to mnemonic (automatically adds BIP39 checksum) -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 mnemonic = bip39.entropyToMnemonic(entropy);\ -\ - -\f3\b \cf5 \strokec5 return -\f5\b0 \cf4 \strokec4 mnemonic;\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 /**\ - * Parse from hex string (for testing)\ - */ -\f5\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 decodeCompactSeedQRFromHex(hexEntropy: \cf8 \strokec8 string\cf4 \strokec4 ): \cf8 \strokec8 string\cf4 \strokec4 \{\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropy = Buffer.from(hexEntropy, \cf6 \strokec6 "hex"\cf4 \strokec4 );\ - -\f3\b \cf5 \strokec5 return -\f5\b0 \cf4 \strokec4 decodeCompactSeedQR(entropy);\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // TESTS -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ -\ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 async -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 runBidirectionalTests() \{\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 " -\f8 \uc0\u55358 \u56810 -\f5 CompactSeedQR Bidirectional Tests\\n"\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\ -\ - -\f3\b \cf5 \strokec5 let -\f5\b0 \cf4 \strokec4 passed = \cf8 \strokec8 0\cf4 \strokec4 ;\ - -\f3\b \cf5 \strokec5 let -\f5\b0 \cf4 \strokec4 failed = \cf8 \strokec8 0\cf4 \strokec4 ;\ -\ - -\f3\b \cf5 \strokec5 for -\f5\b0 \cf4 \strokec4 ( -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 vector -\f3\b \cf5 \strokec5 of -\f5\b0 \cf4 \strokec4 TEST_VECTORS) \{\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `\\n -\f8 \uc0\u55357 \u56523 -\f5 \cf4 \strokec4 $\{vector.name\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `Mnemonic: \cf4 \strokec4 $\{vector.mnemonic.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ...`\cf4 \strokec4 );\ -\ - -\f3\b \cf5 \strokec5 try -\f5\b0 \cf4 \strokec4 \{\ - -\f6\i \cf9 \strokec9 // ===== DIRECTION 1: ENCODE ===== -\f5\i0 \cf4 \strokec4 \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n -\f8 \uc0\u55357 \u56594 -\f5 ENCODE: Mnemonic -\f9 \uc0\u8594 -\f5 CompactSeedQR"\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 encodedEntropy = encodeToCompactSeedQR(vector.mnemonic);\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 encodedHex = encodedEntropy.toString(\cf6 \strokec6 "hex"\cf4 \strokec4 );\ - \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Generated entropy: \cf4 \strokec4 $\{encodedHex\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Expected entropy: \cf4 \strokec4 $\{vector.hexEntropy\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 encodeMatches = encodedHex === vector.hexEntropy;\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{encodeMatches ? \cf6 \strokec6 " -\f8 \uc0\u9989 -\f5 "\cf4 \strokec4 : \cf6 \strokec6 " -\f8 \uc0\u10060 -\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Encode \cf4 \strokec4 $\{encodeMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\ -\ - -\f6\i \cf9 \strokec9 // Generate actual QR code -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 qrDataURL = -\f3\b \cf5 \strokec5 await -\f5\b0 \cf4 \strokec4 generateCompactSeedQR(vector.mnemonic);\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` -\f8 \uc0\u55357 \u56561 -\f5 QR Code generated (\cf4 \strokec4 $\{qrDataURL.length\}\cf6 \strokec6 bytes PNG data)`\cf4 \strokec4 );\ -\ - -\f6\i \cf9 \strokec9 // ===== DIRECTION 2: DECODE ===== -\f5\i0 \cf4 \strokec4 \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n -\f8 \uc0\u55357 \u56595 -\f5 DECODE: CompactSeedQR -\f9 \uc0\u8594 -\f5 Mnemonic"\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 decodedMnemonic = decodeCompactSeedQRFromHex(vector.hexEntropy);\ - \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Decoded: \cf4 \strokec4 $\{decodedMnemonic.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ...`\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 decodeMatches = decodedMnemonic === vector.mnemonic;\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{decodeMatches ? \cf6 \strokec6 " -\f8 \uc0\u9989 -\f5 "\cf4 \strokec4 : \cf6 \strokec6 " -\f8 \uc0\u10060 -\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Decode \cf4 \strokec4 $\{decodeMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\ -\ - -\f6\i \cf9 \strokec9 // ===== ROUND-TRIP TEST ===== -\f5\i0 \cf4 \strokec4 \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n -\f8 \uc0\u55357 \u56580 -\f5 ROUND-TRIP: Mnemonic -\f9 \uc0\u8594 -\f5 QR -\f9 \uc0\u8594 -\f5 Mnemonic"\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 roundTripMnemonic = decodeCompactSeedQR(encodedEntropy);\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 roundTripMatches = roundTripMnemonic === vector.mnemonic;\ - \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` \cf4 \strokec4 $\{roundTripMatches ? \cf6 \strokec6 " -\f8 \uc0\u9989 -\f5 "\cf4 \strokec4 : \cf6 \strokec6 " -\f8 \uc0\u10060 -\f5 "\cf4 \strokec4 \}\cf6 \strokec6 Round-trip \cf4 \strokec4 $\{roundTripMatches ? \cf6 \strokec6 "PASSED"\cf4 \strokec4 : \cf6 \strokec6 "FAILED"\cf4 \strokec4 \}\cf6 \strokec6 `\cf4 \strokec4 );\ -\ - -\f3\b \cf5 \strokec5 if -\f5\b0 \cf4 \strokec4 (encodeMatches && decodeMatches && roundTripMatches) \{\ - passed++;\ - \} -\f3\b \cf5 \strokec5 else -\f5\b0 \cf4 \strokec4 \{\ - failed++;\ - \}\ -\ - \} -\f3\b \cf5 \strokec5 catch -\f5\b0 \cf4 \strokec4 (error) \{\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` -\f8 \uc0\u10060 -\f5 ERROR: \cf4 \strokec4 $\{error\}\cf6 \strokec6 `\cf4 \strokec4 );\ - failed++;\ - \}\ - \}\ -\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n"\cf4 \strokec4 + \cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 `\\n -\f8 \uc0\u55357 \u56522 -\f5 Results: \cf4 \strokec4 $\{passed\}\cf6 \strokec6 /\cf4 \strokec4 $\{TEST_VECTORS.length\}\cf6 \strokec6 passed`\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 if -\f5\b0 \cf4 \strokec4 (failed === \cf8 \strokec8 0\cf4 \strokec4 ) \{\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 " -\f8 \uc0\u55356 \u57225 -\f5 ALL TESTS PASSED!"\cf4 \strokec4 );\ - \}\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // EXAMPLE USAGE -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // ============================================================================ -\f5\i0 \cf4 \strokec4 \ -\ -\pard\pardeftab720\partightenfactor0 - -\f3\b \cf5 \strokec5 async -\f5\b0 \cf4 \strokec4 -\f3\b \cf5 \strokec5 function -\f5\b0 \cf4 \strokec4 demonstrateUsage() \{\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n\\n"\cf4 \strokec4 + \cf6 \strokec6 "="\cf4 \strokec4 .repeat(\cf8 \strokec8 80\cf4 \strokec4 ));\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 " -\f8 \uc0\u55357 \u56481 -\f5 USAGE EXAMPLE\\n"\cf4 \strokec4 );\ -\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 exampleMnemonic = \cf6 \strokec6 "forum undo fragile fade shy sign arrest garment culture tube off merit"\cf4 \strokec4 ;\ -\ - -\f6\i \cf9 \strokec9 // ===== CREATE QR CODE ===== -\f5\i0 \cf4 \strokec4 \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 " -\f8 1\uc0\u65039 \u8419 -\f5 CREATE CompactSeedQR from mnemonic:"\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Input: \cf4 \strokec4 $\{exampleMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 entropy = encodeToCompactSeedQR(exampleMnemonic);\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Binary data (hex): \cf4 \strokec4 $\{entropy.toString(\cf6 \strokec6 "hex"\cf4 \strokec4 )\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Binary data (bytes): [\cf4 \strokec4 $\{\cf8 \strokec8 Array\cf4 \strokec4 .from(entropy).join(\cf6 \strokec6 ", "\cf4 \strokec4 )\}\cf6 \strokec6 ]`\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Length: \cf4 \strokec4 $\{entropy.length\}\cf6 \strokec6 bytes (\cf4 \strokec4 $\{entropy.length === \cf8 \strokec8 16\cf4 \strokec4 ? \cf6 \strokec6 "12-word"\cf4 \strokec4 : \cf6 \strokec6 "24-word"\cf4 \strokec4 \}\cf6 \strokec6 )`\cf4 \strokec4 );\ -\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 qrDataURL = -\f3\b \cf5 \strokec5 await -\f5\b0 \cf4 \strokec4 generateCompactSeedQR(exampleMnemonic);\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` -\f8 \uc0\u9989 -\f5 QR code generated!`\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` -\f8 \uc0\u55357 \u56561 -\f5 Display this QR: `\cf4 \strokec4 );\ -\ - -\f6\i \cf9 \strokec9 // ===== SCAN QR CODE ===== -\f5\i0 \cf4 \strokec4 \ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 "\\n -\f8 2\uc0\u65039 \u8419 -\f5 SCAN CompactSeedQR and restore mnemonic:"\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Simulating QR scanner output...`\cf4 \strokec4 );\ - \ - -\f6\i \cf9 \strokec9 // In real app, your QR scanner returns raw bytes: -\f5\i0 \cf4 \strokec4 \ - -\f6\i \cf9 \strokec9 // const scannedBytes = qrScanner.decode(); -\f5\i0 \cf4 \strokec4 \ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 scannedBytes = entropy; -\f6\i \cf9 \strokec9 // Simulate scanning the QR we just created -\f5\i0 \cf4 \strokec4 \ -\ - -\f3\b \cf5 \strokec5 const -\f5\b0 \cf4 \strokec4 restoredMnemonic = decodeCompactSeedQR(scannedBytes);\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` Restored: \cf4 \strokec4 $\{restoredMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\ - \cf8 \strokec8 console\cf4 \strokec4 .log(\cf6 \strokec6 ` -\f8 \uc0\u9989 -\f5 Match: \cf4 \strokec4 $\{restoredMnemonic === exampleMnemonic\}\cf6 \strokec6 `\cf4 \strokec4 );\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f6\i \cf9 \strokec9 // Run everything -\f5\i0 \cf4 \strokec4 \ -runBidirectionalTests().then(() => demonstrateUsage());\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Run Command\ -\pard\pardeftab720\qc\partightenfactor0 - -\f4\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f5\fs26 \cf2 \strokec2 bash\ -\pard\pardeftab720\partightenfactor0 -\cf4 \strokec4 bun run compact-seedqr-complete.ts\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Expected Output\ -\pard\pardeftab720\qc\partightenfactor0 - -\f4\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f5\fs26 \cf2 \strokec2 text\ -\pard\pardeftab720\partightenfactor0 - -\f8 \cf4 \strokec4 \uc0\u55358 \u56810 -\f5 CompactSeedQR Bidirectional Tests\ -\ -================================================================================\ -\ - -\f8 \uc0\u55357 \u56523 -\f5 Test Vector 1: 24-word\ -Mnemonic: attack pizza motion avocado network gather crop fre...\ -\ - -\f8 \uc0\u55357 \u56594 -\f5 ENCODE: Mnemonic -\f9 \uc0\u8594 -\f5 CompactSeedQR\ - Generated entropy: 0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a\ - Expected entropy: 0e74b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a\ - -\f8 \uc0\u9989 -\f5 Encode PASSED\ - -\f8 \uc0\u55357 \u56561 -\f5 QR Code generated (2847 bytes PNG data)\ -\ - -\f8 \uc0\u55357 \u56595 -\f5 DECODE: CompactSeedQR -\f9 \uc0\u8594 -\f5 Mnemonic\ - Decoded: attack pizza motion avocado network gather crop fre...\ - -\f8 \uc0\u9989 -\f5 Decode PASSED\ -\ - -\f8 \uc0\u55357 \u56580 -\f5 ROUND-TRIP: Mnemonic -\f9 \uc0\u8594 -\f5 QR -\f9 \uc0\u8594 -\f5 Mnemonic\ - -\f8 \uc0\u9989 -\f5 Round-trip PASSED\ -\ -...\ -\ -================================================================================\ -\ - -\f8 \uc0\u55357 \u56522 -\f5 Results: 3/3 passed\ - -\f8 \uc0\u55356 \u57225 -\f5 ALL TESTS PASSED!\ -\ -================================================================================\ - -\f8 \uc0\u55357 \u56481 -\f5 USAGE EXAMPLE\ -\ - -\f8 1\uc0\u65039 \u8419 -\f5 CREATE CompactSeedQR from mnemonic:\ - Input: forum undo fragile fade shy sign arrest garment culture tube off merit\ - Binary data (hex): 5bbd9d71a8ec799083laff359d456545\ - Binary data (bytes): [91, 189, 157, 113, 168, 236, 121, ...]\ - Length: 16 bytes (12-word)\ - -\f8 \uc0\u9989 -\f5 QR code generated!\ -\ - -\f8 2\uc0\u65039 \u8419 -\f5 SCAN CompactSeedQR and restore mnemonic:\ - Simulating QR scanner output...\ - Restored: forum undo fragile fade shy sign arrest garment culture tube off merit\ - -\f8 \uc0\u9989 -\f5 Match: true\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Summary: What the Code Does\ - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160 -\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320 -\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480 -\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 - -\fs24 \cf2 \strokec10 Direction\cf0 \cell -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 -\cf2 Input\cf0 \cell -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 -\cf2 Output\cf0 \cell -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 -\cf2 What It Does\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160 -\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320 -\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480 -\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 1. Encode -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Mnemonic (words)\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Binary entropy (16/32 bytes)\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Strips checksum, creates raw bytes for QR\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1000\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2160 -\clvertalc \clshdrawnil \clwWidth2033\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx4320 -\clvertalc \clshdrawnil \clwWidth2779\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx6480 -\clvertalc \clshdrawnil \clwWidth4979\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 2. Decode -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Binary data from QR\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Mnemonic (words)\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Reads raw bytes, adds checksum, converts to words\cf0 \cell \lastrow\row -\pard\pardeftab720\qc\partightenfactor0 - -\f4\fs22 \cf3 \strokec3 \ -\ -\pard\pardeftab720\sa240\partightenfactor0 - -\f0\fs24 \cf2 \strokec2 The QR code itself contains -\f1\b raw binary entropy -\f0\b0 (NOT encoded as hex or base64). This is why CompactSeedQR is smaller than Standard SeedQR!{\field{\*\fldinst{HYPERLINK "https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md"}}{\fldrslt \cf11 \ul \ulc11 \strokec11 github+1}}\ -} \ No newline at end of file diff --git a/REFERENCE/bun-seedQR-implement.md.rtf b/REFERENCE/bun-seedQR-implement.md.rtf deleted file mode 100644 index d66c4e6..0000000 --- a/REFERENCE/bun-seedQR-implement.md.rtf +++ /dev/null @@ -1,470 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2867 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;\f1\froman\fcharset0 Times-Bold;\f2\fnil\fcharset0 HelveticaNeue; -\f3\fmodern\fcharset0 Courier;\f4\fmodern\fcharset0 Courier-Bold;\f5\fmodern\fcharset0 Courier-Oblique; -\f6\fnil\fcharset0 AppleColorEmoji;} -{\colortbl;\red255\green255\blue255;\red255\green255\blue255;\red0\green0\blue0;\red185\green188\blue186; -\red111\green144\blue176;\red162\green127\blue173;\red166\green178\blue85;\red132\green134\blue132;\red212\green128\blue77; -\red0\green0\blue0;\red191\green80\blue83;} -{\*\expandedcolortbl;;\cssrgb\c100000\c100000\c100000;\cssrgb\c0\c0\c0\c84706;\cssrgb\c77255\c78431\c77647; -\cssrgb\c50588\c63529\c74510;\cssrgb\c69804\c58039\c73333;\cssrgb\c70980\c74118\c40784;\cssrgb\c58824\c59608\c58824;\cssrgb\c87059\c57647\c37255; -\cssrgb\c0\c0\c0;\cssrgb\c80000\c40000\c40000;} -\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 -\deftab720 -\pard\pardeftab720\sa240\partightenfactor0 - -\f0\fs24 \cf2 \expnd0\expndtw0\kerning0 -\outl0\strokewidth0 \strokec2 Here's a -\f1\b Bun TypeScript test -\f0\b0 for -\f1\b SeedSigner SeedQR -\f0\b0 (both Standard and Compact formats) using -\f1\b Test Vector 1 -\f0\b0 from the official specification:\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 Setup\ -\pard\pardeftab720\qc\partightenfactor0 - -\f2\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f3\fs26 \cf2 \strokec2 bash\ -\pard\pardeftab720\partightenfactor0 -\cf4 \strokec4 mkdir seedsigner-test\ -\pard\pardeftab720\partightenfactor0 -\cf5 \strokec5 cd\cf4 \strokec4 seedsigner-test\ -bun init -y\ -bun add bip39\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f4\b\fs39 \cf2 \strokec2 seedsigner-test.ts -\f1\fs36 \ -\pard\pardeftab720\qc\partightenfactor0 - -\f2\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f3\fs26 \cf2 \strokec2 typescript\ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 import -\f3\b0 \cf4 \strokec4 * -\f4\b \cf6 \strokec6 as -\f3\b0 \cf4 \strokec4 bip39 -\f4\b \cf6 \strokec6 from -\f3\b0 \cf4 \strokec4 \cf7 \strokec7 "bip39"\cf4 \strokec4 ;\ -\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 BIP39_ENGLISH_WORDLIST = [\ - \cf7 \strokec7 "abandon"\cf4 \strokec4 , \cf7 \strokec7 "ability"\cf4 \strokec4 , \cf7 \strokec7 "able"\cf4 \strokec4 , \cf7 \strokec7 "about"\cf4 \strokec4 , \cf7 \strokec7 "above"\cf4 \strokec4 , \cf7 \strokec7 "absent"\cf4 \strokec4 , \cf7 \strokec7 "absorb"\cf4 \strokec4 , \cf7 \strokec7 "abstract"\cf4 \strokec4 , \cf7 \strokec7 "absurd"\cf4 \strokec4 , \cf7 \strokec7 "abuse"\cf4 \strokec4 ,\ - \cf7 \strokec7 "access"\cf4 \strokec4 , \cf7 \strokec7 "accident"\cf4 \strokec4 , \cf7 \strokec7 "account"\cf4 \strokec4 , \cf7 \strokec7 "accuse"\cf4 \strokec4 , \cf7 \strokec7 "achieve"\cf4 \strokec4 , \cf7 \strokec7 "acid"\cf4 \strokec4 , \cf7 \strokec7 "acoustic"\cf4 \strokec4 , \cf7 \strokec7 "acquire"\cf4 \strokec4 , \cf7 \strokec7 "across"\cf4 \strokec4 , \cf7 \strokec7 "act"\cf4 \strokec4 ,\ - \cf7 \strokec7 "actor"\cf4 \strokec4 , \cf7 \strokec7 "actress"\cf4 \strokec4 , \cf7 \strokec7 "actual"\cf4 \strokec4 , \cf7 \strokec7 "adapt"\cf4 \strokec4 , \cf7 \strokec7 "add"\cf4 \strokec4 , \cf7 \strokec7 "addict"\cf4 \strokec4 , \cf7 \strokec7 "address"\cf4 \strokec4 , \cf7 \strokec7 "adjust"\cf4 \strokec4 , \cf7 \strokec7 "admit"\cf4 \strokec4 , \cf7 \strokec7 "adult"\cf4 \strokec4 ,\ - \cf7 \strokec7 "advance"\cf4 \strokec4 , \cf7 \strokec7 "advice"\cf4 \strokec4 , \cf7 \strokec7 "aerobic"\cf4 \strokec4 , \cf7 \strokec7 "affair"\cf4 \strokec4 , \cf7 \strokec7 "afford"\cf4 \strokec4 , \cf7 \strokec7 "afraid"\cf4 \strokec4 , \cf7 \strokec7 "again"\cf4 \strokec4 , \cf7 \strokec7 "age"\cf4 \strokec4 , \cf7 \strokec7 "agent"\cf4 \strokec4 , \cf7 \strokec7 "agree"\cf4 \strokec4 ,\ - \cf7 \strokec7 "ahead"\cf4 \strokec4 , \cf7 \strokec7 "aim"\cf4 \strokec4 , \cf7 \strokec7 "air"\cf4 \strokec4 , \cf7 \strokec7 "airport"\cf4 \strokec4 , \cf7 \strokec7 "aisle"\cf4 \strokec4 , \cf7 \strokec7 "alarm"\cf4 \strokec4 , \cf7 \strokec7 "album"\cf4 \strokec4 , \cf7 \strokec7 "alcohol"\cf4 \strokec4 , \cf7 \strokec7 "alert"\cf4 \strokec4 , \cf7 \strokec7 "alien"\cf4 \strokec4 ,\ - \cf7 \strokec7 "all"\cf4 \strokec4 , \cf7 \strokec7 "alley"\cf4 \strokec4 , \cf7 \strokec7 "allow"\cf4 \strokec4 , \cf7 \strokec7 "almost"\cf4 \strokec4 , \cf7 \strokec7 "alone"\cf4 \strokec4 , \cf7 \strokec7 "alpha"\cf4 \strokec4 , \cf7 \strokec7 "already"\cf4 \strokec4 , \cf7 \strokec7 "also"\cf4 \strokec4 , \cf7 \strokec7 "alter"\cf4 \strokec4 , \cf7 \strokec7 "always"\cf4 \strokec4 ,\ - \cf7 \strokec7 "amateur"\cf4 \strokec4 , \cf7 \strokec7 "amazing"\cf4 \strokec4 , \cf7 \strokec7 "among"\cf4 \strokec4 , \cf7 \strokec7 "amount"\cf4 \strokec4 , \cf7 \strokec7 "amused"\cf4 \strokec4 , \cf7 \strokec7 "analyst"\cf4 \strokec4 , \cf7 \strokec7 "anchor"\cf4 \strokec4 , \cf7 \strokec7 "ancient"\cf4 \strokec4 , \cf7 \strokec7 "anger"\cf4 \strokec4 , \cf7 \strokec7 "angle"\cf4 \strokec4 ,\ - \cf7 \strokec7 "angry"\cf4 \strokec4 , \cf7 \strokec7 "animal"\cf4 \strokec4 , \cf7 \strokec7 "ankle"\cf4 \strokec4 , \cf7 \strokec7 "announce"\cf4 \strokec4 , \cf7 \strokec7 "annual"\cf4 \strokec4 , \cf7 \strokec7 "another"\cf4 \strokec4 , \cf7 \strokec7 "answer"\cf4 \strokec4 , \cf7 \strokec7 "antenna"\cf4 \strokec4 , \cf7 \strokec7 "antique"\cf4 \strokec4 , \cf7 \strokec7 "anxiety"\cf4 \strokec4 ,\ - \cf7 \strokec7 "any"\cf4 \strokec4 , \cf7 \strokec7 "apart"\cf4 \strokec4 , \cf7 \strokec7 "apology"\cf4 \strokec4 , \cf7 \strokec7 "appear"\cf4 \strokec4 , \cf7 \strokec7 "apple"\cf4 \strokec4 , \cf7 \strokec7 "approve"\cf4 \strokec4 , \cf7 \strokec7 "april"\cf4 \strokec4 , \cf7 \strokec7 "arch"\cf4 \strokec4 , \cf7 \strokec7 "arctic"\cf4 \strokec4 , \cf7 \strokec7 "area"\cf4 \strokec4 ,\ - \cf7 \strokec7 "arena"\cf4 \strokec4 , \cf7 \strokec7 "argue"\cf4 \strokec4 , \cf7 \strokec7 "arm"\cf4 \strokec4 , \cf7 \strokec7 "armed"\cf4 \strokec4 , \cf7 \strokec7 "armor"\cf4 \strokec4 , \cf7 \strokec7 "army"\cf4 \strokec4 , \cf7 \strokec7 "around"\cf4 \strokec4 , \cf7 \strokec7 "arrange"\cf4 \strokec4 , \cf7 \strokec7 "arrest"\cf4 \strokec4 , \cf7 \strokec7 "arrive"\cf4 \strokec4 ,\ - -\f5\i \cf8 \strokec8 // ... (truncated for brevity - full 2048 word list needed in production) -\f3\i0 \cf4 \strokec4 \ - \cf7 \strokec7 "attack"\cf4 \strokec4 , \cf7 \strokec7 "pizza"\cf4 \strokec4 , \cf7 \strokec7 "motion"\cf4 \strokec4 , \cf7 \strokec7 "avocado"\cf4 \strokec4 , \cf7 \strokec7 "network"\cf4 \strokec4 , \cf7 \strokec7 "gather"\cf4 \strokec4 , \cf7 \strokec7 "crop"\cf4 \strokec4 , \cf7 \strokec7 "fresh"\cf4 \strokec4 , \cf7 \strokec7 "patrol"\cf4 \strokec4 , \cf7 \strokec7 "unusual"\cf4 \strokec4 ,\ - -\f5\i \cf8 \strokec8 // Full wordlist should be loaded from bip39 library -\f3\i0 \cf4 \strokec4 \ -];\ -\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // Full wordlist from bip39 -\f3\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 FULL_WORDLIST = bip39.wordlists.english;\ -\ - -\f4\b \cf6 \strokec6 interface -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 SeedQRResult\cf4 \strokec4 \{\ - mnemonic: \cf9 \strokec9 string\cf4 \strokec4 ;\ - wordCount: \cf9 \strokec9 12\cf4 \strokec4 | \cf9 \strokec9 24\cf4 \strokec4 ;\ - format: \cf7 \strokec7 "standard"\cf4 \strokec4 | \cf7 \strokec7 "compact"\cf4 \strokec4 ;\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // --- Standard SeedQR: Parse numeric digit stream --- -\f3\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 function -\f3\b0 \cf4 \strokec4 parseStandardSeedQR(digitStream: \cf9 \strokec9 string\cf4 \strokec4 ): SeedQRResult \{\ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (digitStream.length % \cf9 \strokec9 4\cf4 \strokec4 !== \cf9 \strokec9 0\cf4 \strokec4 ) \{\ - -\f4\b \cf6 \strokec6 throw -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 new -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 Error\cf4 \strokec4 (\cf7 \strokec7 "Invalid digit stream length. Must be multiple of 4."\cf4 \strokec4 );\ - \}\ -\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 wordIndices: \cf9 \strokec9 number\cf4 \strokec4 [] = [];\ - \ - -\f5\i \cf8 \strokec8 // Split into 4-digit indices -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 for -\f3\b0 \cf4 \strokec4 ( -\f4\b \cf6 \strokec6 let -\f3\b0 \cf4 \strokec4 i = \cf9 \strokec9 0\cf4 \strokec4 ; i < digitStream.length; i += \cf9 \strokec9 4\cf4 \strokec4 ) \{\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 indexStr = digitStream.slice(i, i + \cf9 \strokec9 4\cf4 \strokec4 );\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 index = parseInt(indexStr, \cf9 \strokec9 10\cf4 \strokec4 );\ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (isNaN(index) || index >= \cf9 \strokec9 2048\cf4 \strokec4 ) \{\ - -\f4\b \cf6 \strokec6 throw -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 new -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 Error\cf4 \strokec4 (\cf7 \strokec7 `Invalid word index: \cf4 \strokec4 $\{indexStr\}\cf7 \strokec7 `\cf4 \strokec4 );\ - \}\ - wordIndices.push(index);\ - \}\ -\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 wordCount = wordIndices.length;\ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (wordCount !== \cf9 \strokec9 12\cf4 \strokec4 && wordCount !== \cf9 \strokec9 24\cf4 \strokec4 ) \{\ - -\f4\b \cf6 \strokec6 throw -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 new -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 Error\cf4 \strokec4 (\cf7 \strokec7 `Invalid word count: \cf4 \strokec4 $\{wordCount\}\cf7 \strokec7 . Must be 12 or 24.`\cf4 \strokec4 );\ - \}\ -\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 mnemonicWords = wordIndices.map(index => FULL_WORDLIST[index]);\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 mnemonic = mnemonicWords.join(\cf7 \strokec7 " "\cf4 \strokec4 );\ -\ - -\f4\b \cf6 \strokec6 return -\f3\b0 \cf4 \strokec4 \{\ - mnemonic,\ - wordCount,\ - format: \cf7 \strokec7 "standard"\cf4 \strokec4 \ - \};\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // --- Compact SeedQR: Parse binary entropy (128 bits for 12-word, 256 bits for 24-word) --- -\f3\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 function -\f3\b0 \cf4 \strokec4 parseCompactSeedQR(hexEntropy: \cf9 \strokec9 string\cf4 \strokec4 ): SeedQRResult \{\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 entropy = Buffer.from(hexEntropy, \cf7 \strokec7 'hex'\cf4 \strokec4 );\ - \ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (entropy.length !== \cf9 \strokec9 16\cf4 \strokec4 && entropy.length !== \cf9 \strokec9 32\cf4 \strokec4 ) \{\ - -\f4\b \cf6 \strokec6 throw -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 new -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 Error\cf4 \strokec4 (\cf7 \strokec7 `Invalid entropy length: \cf4 \strokec4 $\{entropy.length\}\cf7 \strokec7 . Must be 16 (12-word) or 32 (24-word) bytes.`\cf4 \strokec4 );\ - \}\ -\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 wordCount = entropy.length === \cf9 \strokec9 16\cf4 \strokec4 ? \cf9 \strokec9 12\cf4 \strokec4 : \cf9 \strokec9 24\cf4 \strokec4 ;\ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 mnemonic = bip39.entropyToMnemonic(entropy);\ -\ - -\f4\b \cf6 \strokec6 return -\f3\b0 \cf4 \strokec4 \{\ - mnemonic,\ - wordCount,\ - format: \cf7 \strokec7 "compact"\cf4 \strokec4 \ - \};\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // --- Test Vector 1 --- -\f3\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 async -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 function -\f3\b0 \cf4 \strokec4 runSeedSignerTests() \{\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 " -\f6 \uc0\u55358 \u56785 \u8205 \u55357 \u56620 -\f3 Testing SeedSigner SeedQR Format\\n"\cf4 \strokec4 );\ -\ - -\f5\i \cf8 \strokec8 // Test Vector 1: 24-word seed -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 testVector1 = \{\ - mnemonic: \cf7 \strokec7 "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire"\cf4 \strokec4 ,\ - standardDigitStream: \cf7 \strokec7 "011513251154012711900771041507421289190620080870026613431420201617920614089619290300152408010643"\cf4 \strokec4 ,\ - compactHexEntropy: \cf7 \strokec7 "0e54b64107f94cc0ccfae6a13dcbec3662154fec67e0e00999c07892597d190a"\cf4 \strokec4 \ - \};\ -\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 " -\f6 \uc0\u55357 \u56522 -\f3 Test Vector 1 (24-word)"\cf4 \strokec4 );\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 "Expected:"\cf4 \strokec4 , testVector1.mnemonic);\ -\ - -\f4\b \cf6 \strokec6 try -\f3\b0 \cf4 \strokec4 \{\ - -\f5\i \cf8 \strokec8 // Test Standard SeedQR -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 standardResult = parseStandardSeedQR(testVector1.standardDigitStream);\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 "\\n -\f6 \uc0\u9989 -\f3 Standard SeedQR PASSED"\cf4 \strokec4 );\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 ` Words: \cf4 \strokec4 $\{standardResult.wordCount\}\cf7 \strokec7 , Mnemonic: \cf4 \strokec4 $\{standardResult.mnemonic.slice(\cf9 \strokec9 0\cf4 \strokec4 , \cf9 \strokec9 50\cf4 \strokec4 )\}\cf7 \strokec7 ...`\cf4 \strokec4 );\ -\ - -\f5\i \cf8 \strokec8 // Test Compact SeedQR -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 const -\f3\b0 \cf4 \strokec4 compactResult = parseCompactSeedQR(testVector1.compactHexEntropy);\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 "\\n -\f6 \uc0\u9989 -\f3 Compact SeedQR PASSED"\cf4 \strokec4 );\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 ` Words: \cf4 \strokec4 $\{compactResult.wordCount\}\cf7 \strokec7 , Mnemonic: \cf4 \strokec4 $\{compactResult.mnemonic.slice(\cf9 \strokec9 0\cf4 \strokec4 , \cf9 \strokec9 50\cf4 \strokec4 )\}\cf7 \strokec7 ...`\cf4 \strokec4 );\ -\ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (standardResult.mnemonic === testVector1.mnemonic && compactResult.mnemonic === testVector1.mnemonic) \{\ - \cf9 \strokec9 console\cf4 \strokec4 .log(\cf7 \strokec7 "\\n -\f6 \uc0\u55356 \u57225 -\f3 ALL TESTS PASSED!"\cf4 \strokec4 );\ - \}\ -\ - \} -\f4\b \cf6 \strokec6 catch -\f3\b0 \cf4 \strokec4 (error) \{\ - \cf9 \strokec9 console\cf4 \strokec4 .error(\cf7 \strokec7 " -\f6 \uc0\u10060 -\f3 Test failed:"\cf4 \strokec4 , error);\ - \}\ -\}\ -\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // Run tests -\f3\i0 \cf4 \strokec4 \ -runSeedSignerTests();\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Run Command\ -\pard\pardeftab720\qc\partightenfactor0 - -\f2\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f3\fs26 \cf2 \strokec2 bash\ -\pard\pardeftab720\partightenfactor0 -\cf4 \strokec4 bun run seedsigner-test.ts\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Expected Output\ -\pard\pardeftab720\qc\partightenfactor0 - -\f2\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f3\fs26 \cf2 \strokec2 text\ -\pard\pardeftab720\partightenfactor0 - -\f6 \cf4 \strokec4 \uc0\u55358 \u56785 \u8205 \u55357 \u56620 -\f3 Testing SeedSigner SeedQR Format\ -\ - -\f6 \uc0\u55357 \u56522 -\f3 Test Vector 1 (24-word)\ -Expected: attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire\ -\ - -\f6 \uc0\u9989 -\f3 Standard SeedQR PASSED\ - Words: 24, Mnemonic: attack pizza motion avocado network gather crop f...\ -\ - -\f6 \uc0\u9989 -\f3 Compact SeedQR PASSED\ - Words: 24, Mnemonic: attack pizza motion avocado network gather crop f...\ -\ - -\f6 \uc0\u55356 \u57225 -\f3 ALL TESTS PASSED!\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Key Differences from Krux KEF\ - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrt\brdrnil \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 - -\fs24 \cf2 \strokec10 Feature\cf0 \cell -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 -\cf2 SeedSigner SeedQR\cf0 \cell -\pard\intbl\itap1\pardeftab720\qc\partightenfactor0 -\cf2 Krux KEF\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Purpose -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Encode BIP39 mnemonic indices\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Encrypt arbitrary data\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 Encryption -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 None -\f0\b0 (plain text)\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 AES-GCM + PBKDF2\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 Formats -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Standard (digits), Compact (binary)\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Versioned envelopes\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 Input -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 QR contains raw indices/entropy\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Base43-encoded encrypted data\cf0 \cell \row - -\itap1\trowd \taflags0 \trgaph108\trleft-108 \trbrdrl\brdrnil \trbrdrt\brdrnil \trbrdrr\brdrnil -\clvertalc \clshdrawnil \clwWidth1160\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx2880 -\clvertalc \clshdrawnil \clwWidth3425\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx5760 -\clvertalc \clshdrawnil \clwWidth3052\clftsWidth3 \clmart10 \clmarl10 \clmarb10 \clmarr10 \clbrdrt\brdrnil \clbrdrl\brdrnil \clbrdrb\brdrnil \clbrdrr\brdrnil \clpadt20 \clpadl20 \clpadb20 \clpadr20 \gaph\cellx8640 -\pard\intbl\itap1\pardeftab720\partightenfactor0 - -\f1\b \cf2 Output -\f0\b0 \cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 BIP39 mnemonic\cf0 \cell -\pard\intbl\itap1\pardeftab720\partightenfactor0 -\cf2 Raw decrypted bytes\cf0 \cell \lastrow\row -\pard\pardeftab720\qc\partightenfactor0 - -\f2\fs22 \cf3 \strokec3 \ -\ -\pard\pardeftab720\sa298\partightenfactor0 - -\f1\b\fs36 \cf2 \strokec2 Usage in Your App\ -\pard\pardeftab720\qc\partightenfactor0 - -\f2\b0\fs22 \cf3 \strokec3 \ -\pard\pardeftab720\partightenfactor0 - -\f3\fs26 \cf2 \strokec2 typescript\ -\pard\pardeftab720\partightenfactor0 - -\f5\i \cf8 \strokec8 // Detect format automatically -\f3\i0 \cf4 \strokec4 \ -\pard\pardeftab720\partightenfactor0 - -\f4\b \cf6 \strokec6 export -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 async -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 function -\f3\b0 \cf4 \strokec4 parseSeedQR(qrData: \cf9 \strokec9 string\cf4 \strokec4 ): \cf9 \strokec9 Promise\cf4 \strokec4 <\cf9 \strokec9 string\cf4 \strokec4 > \{\ - -\f5\i \cf8 \strokec8 // Check if it's numeric digits (Standard SeedQR) -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (\cf11 \strokec11 /^\\d+$/\cf4 \strokec4 .test(qrData)) \{\ - -\f4\b \cf6 \strokec6 return -\f3\b0 \cf4 \strokec4 parseStandardSeedQR(qrData).mnemonic;\ - \}\ - \ - -\f5\i \cf8 \strokec8 // Check if it's hex (Compact SeedQR) -\f3\i0 \cf4 \strokec4 \ - -\f4\b \cf6 \strokec6 if -\f3\b0 \cf4 \strokec4 (\cf11 \strokec11 /^[0-9a-fA-F]+$/\cf4 \strokec4 .test(qrData)) \{\ - -\f4\b \cf6 \strokec6 return -\f3\b0 \cf4 \strokec4 parseCompactSeedQR(qrData).mnemonic;\ - \}\ - \ - -\f4\b \cf6 \strokec6 throw -\f3\b0 \cf4 \strokec4 -\f4\b \cf6 \strokec6 new -\f3\b0 \cf4 \strokec4 \cf5 \strokec5 Error\cf4 \strokec4 (\cf7 \strokec7 "Unsupported SeedQR format"\cf4 \strokec4 );\ -\}\ -\pard\pardeftab720\sa240\partightenfactor0 - -\f0\fs24 \cf2 \strokec2 The -\f3\fs26 bip39 -\f0\fs24 library handles the full English wordlist and checksum validation automatically!\ -} \ No newline at end of file diff --git a/REFERENCE/encryption_ui.py b/REFERENCE/encryption_ui.py deleted file mode 100644 index e0bd4c1..0000000 --- a/REFERENCE/encryption_ui.py +++ /dev/null @@ -1,715 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021-2024 Krux contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import time -from embit import bip39 -from binascii import hexlify -from ..display import DEFAULT_PADDING, FONT_HEIGHT, BOTTOM_PROMPT_LINE -from ..krux_settings import t, Settings -from ..encryption import QR_CODE_ITER_MULTIPLE -from krux import kef -from ..themes import theme -from . import ( - Page, - Menu, - MENU_CONTINUE, - ESC_KEY, - LETTERS, - UPPERCASE_LETTERS, - NUM_SPECIAL_1, - NUM_SPECIAL_2, - DIGITS, -) - -# Override constants for KEF envelope operations -OVERRIDE_ITERATIONS = 1 -OVERRIDE_VERSION = 2 -OVERRIDE_MODE = 3 -OVERRIDE_LABEL = 4 - -ENCRYPTION_KEY_MAX_LEN = 200 - - -def decrypt_kef(ctx, data): - """finds kef-envelope and returns data fully decrypted, else ValueError""" - from binascii import unhexlify - from krux.baseconv import base_decode, hint_encodings - - # nothing to decrypt or declined raises ValueError here, - # so callers can `except ValueError: pass`, then treat original data. - # If user decides to decrypt and fails with wrong key, then - # `KeyError("Failed to decrypt")` raised by `KEFEnvelope.unseal_ui()` - # will bubble up to caller. - err = "Not decrypted" # intentionally vague - - # if data is str, assume encoded, look for kef envelope - kef_envelope = None - if isinstance(data, str): - encodings = hint_encodings(data) - for encoding in encodings: - as_bytes = None - if encoding in ("hex", "HEX"): - try: - as_bytes = unhexlify(data) - except: - continue - elif encoding == 32: - try: - as_bytes = base_decode(data, 32) - except: - continue - elif encoding == 64: - try: - as_bytes = base_decode(data, 64) - except: - continue - elif encoding == 43: - try: - as_bytes = base_decode(data, 43) - except: - continue - - if as_bytes: - kef_envelope = KEFEnvelope(ctx) - if kef_envelope.parse(as_bytes): - break - kef_envelope = None - del as_bytes - - # kef_envelope may already be parsed, else do so or fail early - if kef_envelope is None: - if not isinstance(data, bytes): - raise ValueError(err) - - kef_envelope = KEFEnvelope(ctx) - if not kef_envelope.parse(data): - raise ValueError(err) - - # unpack as many kef_envelopes as there may be - while True: - data = kef_envelope.unseal_ui() - if data is None: - # fail if not unsealed - raise ValueError(err) - # we may have unsealed another envelope - kef_envelope = KEFEnvelope(ctx) - if not kef_envelope.parse(data): - return data - raise ValueError(err) - - -def prompt_for_text_update( - ctx, - dflt_value, - dflt_prompt=None, - dflt_affirm=True, - prompt_highlight_prefix="", - title=None, - keypads=None, - esc_prompt=False, -): - """Clears screen, prompts question, allows for keypad input""" - if dflt_value: - if dflt_prompt: - dflt_prompt += " " + dflt_value - else: - dflt_prompt = t("Use current value?") + " " + dflt_value - ctx.display.clear() - if dflt_value and dflt_prompt: - ctx.display.draw_centered_text( - dflt_prompt, highlight_prefix=prompt_highlight_prefix - ) - dflt_answer = Page(ctx).prompt("", BOTTOM_PROMPT_LINE) - if dflt_affirm == dflt_answer: - return dflt_value - if not isinstance(keypads, list) or keypads is None: - keypads = [LETTERS, UPPERCASE_LETTERS, NUM_SPECIAL_1, NUM_SPECIAL_2] - value = Page(ctx).capture_from_keypad( - title, keypads, starting_buffer=dflt_value, esc_prompt=esc_prompt - ) - if isinstance(value, str): - return value - return dflt_value - - -class KEFEnvelope(Page): - """UI to handle KEF-Encryption-Format Envelopes""" - - def __init__(self, ctx): - super().__init__(ctx, None) - self.ctx = ctx - self.__key = None - self.__iv = None - self.label = None - self.iterations = Settings().encryption.pbkdf2_iterations - max_delta = self.iterations // 10 - self.iterations += int(time.ticks_ms()) % max_delta - self.mode_name = Settings().encryption.version - self.mode = kef.MODE_NUMBERS[self.mode_name] - self.iv_len = kef.MODE_IVS.get(self.mode, 0) - self.version = None - self.version_name = None - self.ciphertext = None - - def parse(self, kef_envelope): - """parses envelope, from kef.wrap()""" - if self.ciphertext is not None: - raise ValueError("KEF Envelope already parsed") - try: - self.label, self.version, self.iterations, self.ciphertext = kef.unwrap( - kef_envelope - ) - except: - return False - self.version_name = kef.VERSIONS[self.version]["name"] - self.mode = kef.VERSIONS[self.version]["mode"] - self.mode_name = [k for k, v in kef.MODE_NUMBERS.items() if v == self.mode][0] - return True - - def input_key_ui(self, creating=True): - """calls ui to gather master key""" - ui = EncryptionKey(self.ctx) - self.__key = ui.encryption_key(creating) - return bool(self.__key) - - def input_mode_ui(self): - """implements ui to allow user to select KEF mode-of-operation""" - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Use default Mode?") + " " + self.mode_name, highlight_prefix="?" - ) - if self.prompt("", BOTTOM_PROMPT_LINE): - return True - menu_items = [(k, v) for k, v in kef.MODE_NUMBERS.items() if v is not None] - idx, _ = Menu( - self.ctx, [(x[0], lambda: None) for x in menu_items], back_label=None - ).run_loop() - self.mode_name, self.mode = menu_items[idx] - self.iv_len = kef.MODE_IVS.get(self.mode, 0) - return True - - def input_version_ui(self): - """implements ui to allow user to select KEF version""" - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Use default Mode?") + " " + self.mode_name, highlight_prefix="?" - ) - if self.prompt("", BOTTOM_PROMPT_LINE): - return True - menu_items = [ - (v["name"], k) - for k, v in sorted(kef.VERSIONS.items()) - if isinstance(v, dict) and v["mode"] is not None - ] - idx, _ = Menu( - self.ctx, [(x[0], lambda: None) for x in menu_items], back_label=None - ).run_loop() - self.version = [v for i, (_, v) in enumerate(menu_items) if i == idx][0] - self.version_name = kef.VERSIONS[self.version]["name"] - self.mode = kef.VERSIONS[self.version]["mode"] - self.mode_name = [k for k, v in kef.MODE_NUMBERS.items() if v == self.mode][0] - self.iv_len = kef.MODE_IVS.get(self.mode, 0) - return True - - def input_iterations_ui(self): - """implements ui to allow user to set key-stretch iterations""" - curr_value = str(self.iterations) - dflt_prompt = t("Use default PBKDF2 iter.?") - title = t("PBKDF2 iter.") + ": 10K - 510K" - keypads = [DIGITS] - iterations = prompt_for_text_update( - self.ctx, curr_value, dflt_prompt, True, "?", title, keypads - ) - if QR_CODE_ITER_MULTIPLE <= int(iterations) <= 550000: - self.iterations = int(iterations) - return True - return None - - def input_label_ui( - self, - dflt_label="", - dflt_prompt="", - dflt_affirm=True, - title=t("Visible Label"), - keypads=None, - ): - """implements ui to allow user to set a KEF label""" - if dflt_label and not dflt_prompt: - dflt_prompt = t("Update KEF ID?") - dflt_affirm = False - self.label = prompt_for_text_update( - self.ctx, dflt_label, dflt_prompt, dflt_affirm, "?", title, keypads - ) - return True - - def input_iv_ui(self): - """implements ui to allow user to gather entropy from camera for iv""" - if self.iv_len > 0: - error_txt = t("Failed gathering camera entropy") - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Additional entropy from camera required for %s") % self.mode_name - ) - if not self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE): - self.flash_error(error_txt) - self.__iv = None - return None - from .capture_entropy import CameraEntropy - - camera_entropy = CameraEntropy(self.ctx) - entropy = camera_entropy.capture(show_entropy_details=False) - if entropy is None: - self.flash_error(error_txt) - self.__iv = None - return None - self.__iv = entropy[: self.iv_len] - return True - self.__iv = None - return True - - def public_info_ui(self, kef_envelope=None, prompt_decrypt=False): - """implements ui to allow user to see public exterior of KEF envelope""" - if kef_envelope: - self.parse(kef_envelope) - elif not self.ciphertext: - raise ValueError("KEF Envelope not yet parsed") - try: - displayable_label = self.label.decode() - except: - displayable_label = "0x" + hexlify(self.label).decode() - - public_info = "\n".join( - [ - t("KEF Encrypted") + " (" + str(len(self.ciphertext)) + " B)", - self.fit_to_line(displayable_label, t("ID") + ": "), - t("Version") + ": " + self.version_name, - t("PBKDF2 iter.") + ": " + str(self.iterations), - ] - ) - self.ctx.display.clear() - if prompt_decrypt: - return self.prompt( - public_info + "\n\n" + t("Decrypt?"), self.ctx.display.height() // 2 - ) - self.ctx.display.draw_hcentered_text(public_info) - self.ctx.input.wait_for_button() - return True - - def seal_ui( - self, - plaintext, - overrides=None, - dflt_label_prompt="", - dflt_label_affirm=True, - ): - """implements ui to allow user to seal plaintext inside a KEF envelope""" - if not isinstance(overrides, list): - overrides = [] - if self.ciphertext: - raise ValueError("KEF Envelope already sealed") - if not (self.__key or self.input_key_ui()): - return None - if overrides: - if OVERRIDE_ITERATIONS in overrides and not self.input_iterations_ui(): - return None - if OVERRIDE_VERSION in overrides and not self.input_version_ui(): - return None - if OVERRIDE_MODE in overrides and not self.input_mode_ui(): - return None - if self.iv_len: - if not (self.__iv or self.input_iv_ui()): - return None - if OVERRIDE_LABEL in overrides or not self.label: - self.input_label_ui(self.label, dflt_label_prompt, dflt_label_affirm) - if self.version is None: - self.version = kef.suggest_versions(plaintext, self.mode_name)[0] - self.version_name = kef.VERSIONS[self.version]["name"] - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Processing…")) - cipher = kef.Cipher(self.__key, self.label, self.iterations) - self.ciphertext = cipher.encrypt(plaintext, self.version, self.__iv) - self.__key = None - self.__iv = None - return kef.wrap(self.label, self.version, self.iterations, self.ciphertext) - - def unseal_ui(self, kef_envelope=None, prompt_decrypt=True, display_plain=False): - """implements ui to allow user to unseal a plaintext from a sealed KEF envelope""" - if kef_envelope: - if not self.parse(kef_envelope): - return None - if not self.ciphertext: - raise ValueError("KEF Envelope not yet parsed") - if prompt_decrypt: - if not self.public_info_ui(prompt_decrypt=prompt_decrypt): - return None - if not (self.__key or self.input_key_ui(creating=False)): - return None - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Processing…")) - cipher = kef.Cipher(self.__key, self.label, self.iterations) - plaintext = cipher.decrypt(self.ciphertext, self.version) - self.__key = None - if plaintext is None: - raise KeyError("Failed to decrypt") - if display_plain: - self.ctx.display.clear() - try: - self.ctx.display.draw_centered_text(plaintext.decode()) - except: - self.ctx.display.draw_centered_text("0x" + hexlify(plaintext).decode()) - self.ctx.input.wait_for_button() - return plaintext - - -class EncryptionKey(Page): - """UI to capture an encryption key""" - - def __init__(self, ctx): - super().__init__(ctx, None) - self.ctx = ctx - - def key_strength(self, key_string): - """Check the strength of a key.""" - - if isinstance(key_string, bytes): - key_string = hexlify(key_string).decode() - - if len(key_string) < 8: - return t("Weak") - - has_upper = has_lower = has_digit = has_special = False - - for c in key_string: - if "a" <= c <= "z": - has_lower = True - elif "A" <= c <= "Z": - has_upper = True - elif "0" <= c <= "9": - has_digit = True - else: - has_special = True - - # small optimization: stop if all found - if has_upper and has_lower and has_digit and has_special: - break - - # Count how many character types are present - score = sum([has_upper, has_lower, has_digit, has_special]) - - # Add length score to score - key_len = len(key_string) - if key_len >= 12: - score += 1 - if key_len >= 16: - score += 1 - if key_len >= 20: - score += 1 - if key_len >= 40: - score += 1 - - set_len = len(set(key_string)) - if set_len < 6: - score -= 1 - if set_len < 3: - score -= 1 - - # Determine key strength - if score >= 4: - return t("Strong") - if score >= 3: - return t("Medium") - return t("Weak") - - def encryption_key(self, creating=False): - """Loads and returns an encryption key from keypad or QR code""" - submenu = Menu( - self.ctx, - [ - (t("Type Key"), self.load_key), - (t("Scan Key QR Code"), self.load_qr_encryption_key), - ], - back_label=None, - ) - _, key = submenu.run_loop() - - try: - # encryption key may have been encrypted - decrypted = decrypt_kef(self.ctx, key) - try: - # no assumed decodings except for utf8 - decrypted = decrypted.decode() - except: - pass - - key = decrypted if decrypted else key - except KeyError: - self.flash_error(t("Failed to decrypt")) - return None - except ValueError: - # ValueError=not KEF or declined to decrypt - pass - - while True: - if key in (None, "", b"", ESC_KEY, MENU_CONTINUE): - self.flash_error(t("Failed to load")) - return None - - self.ctx.display.clear() - offset_y = DEFAULT_PADDING - displayable = key if isinstance(key, str) else "0x" + hexlify(key).decode() - key_lines = self.ctx.display.draw_hcentered_text( - "{} ({}): {}".format(t("Key"), len(key), displayable), - offset_y, - highlight_prefix=":", - ) - - if creating: - strength = self.key_strength(key) - offset_y += (key_lines + 1) * FONT_HEIGHT - color = theme.error_color if strength == t("Weak") else theme.fg_color - self.ctx.display.draw_hcentered_text( - "{}: {}".format(t("Strength"), strength), - offset_y, - color, - highlight_prefix=":", - ) - - if self.prompt(t("Proceed?"), BOTTOM_PROMPT_LINE): - return key - - # user did not confirm to proceed - if not isinstance(key, str): - return None - key = self.load_key(key) - - def load_key(self, data=""): - """Loads and returns a key from keypad""" - if not isinstance(data, str): - raise TypeError("load_key() expected str") - data = self.capture_from_keypad( - t("Key"), - [LETTERS, UPPERCASE_LETTERS, NUM_SPECIAL_1, NUM_SPECIAL_2], - starting_buffer=data, - ) - if len(str(data)) > ENCRYPTION_KEY_MAX_LEN: - raise ValueError("Maximum length exceeded (%s)" % ENCRYPTION_KEY_MAX_LEN) - return data - - def load_qr_encryption_key(self): - """Loads and returns a key from a QR code""" - - from .qr_capture import QRCodeCapture - - qr_capture = QRCodeCapture(self.ctx) - data, _ = qr_capture.qr_capture_loop() - if data is None: - return None - if len(data) > ENCRYPTION_KEY_MAX_LEN: - raise ValueError("Maximum length exceeded (%s)" % ENCRYPTION_KEY_MAX_LEN) - return data - - -class EncryptMnemonic(Page): - """UI with mnemonic encryption output options""" - - def __init__(self, ctx): - super().__init__(ctx, None) - self.ctx = ctx - self.mode_name = Settings().encryption.version - - def _encrypt_mnemonic_with_label(self): - """Helper method to encrypt mnemonic with label selection.""" - - kef_envelope = KEFEnvelope(self.ctx) - default_label = self.ctx.wallet.key.fingerprint_hex_str() - kef_envelope.label = default_label - mnemonic_bytes = bip39.mnemonic_to_bytes(self.ctx.wallet.key.mnemonic) - encrypted_data = kef_envelope.seal_ui( - mnemonic_bytes, - overrides=[OVERRIDE_LABEL], - dflt_label_prompt=t("Use fingerprint as ID?"), - dflt_label_affirm=True, - ) - if encrypted_data is None: - return None, None - - mnemonic_id = kef_envelope.label - return encrypted_data, mnemonic_id - - def encrypt_menu(self): - """Menu with mnemonic encryption output options""" - - encrypt_outputs_menu = [ - (t("Store on Flash"), self.store_mnemonic_on_memory), - ( - t("Store on SD Card"), - ( - None - if not self.has_sd_card() - else lambda: self.store_mnemonic_on_memory(True) - ), - ), - (t("Encrypted QR Code"), self.encrypted_qr_code), - ] - submenu = Menu(self.ctx, encrypt_outputs_menu) - _, _ = submenu.run_loop() - return MENU_CONTINUE - - def store_mnemonic_on_memory(self, sd_card=False): - """Save encrypted mnemonic on flash or sd_card""" - - from ..encryption import MnemonicStorage - - encrypted_data, mnemonic_id = self._encrypt_mnemonic_with_label() - if encrypted_data is None: - return - - mnemonic_storage = MnemonicStorage() - if mnemonic_id in mnemonic_storage.list_mnemonics(sd_card): - self.flash_error( - t("ID already exists") + "\n" + t("Encrypted mnemonic was not stored") - ) - del mnemonic_storage - return - - if mnemonic_storage.store_encrypted_kef(mnemonic_id, encrypted_data, sd_card): - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Encrypted mnemonic stored with ID:") + " " + mnemonic_id, - highlight_prefix=":", - ) - else: - self.ctx.display.clear() - self.ctx.display.draw_centered_text( - t("Failed to store mnemonic"), theme.error_color - ) - self.ctx.input.wait_for_button() - del mnemonic_storage - - def encrypted_qr_code(self): - """Exports an encryprted mnemonic QR code""" - - encrypted_data, mnemonic_id = self._encrypt_mnemonic_with_label() - if encrypted_data is None: - return - - from .qr_view import SeedQRView - from ..baseconv import base_encode - - # All currently offered versions should encode to base43 - qr_data = base_encode(encrypted_data, 43) - seed_qr_view = SeedQRView(self.ctx, data=qr_data, title=mnemonic_id) - seed_qr_view.display_qr(allow_export=True) - - -class LoadEncryptedMnemonic(Page): - """UI to load encrypted mnemonics stored on flash and Sd card""" - - def __init__(self, ctx): - super().__init__(ctx, None) - self.ctx = ctx - - def load_from_storage(self, remove_opt=False): - """Lists all encrypted mnemonics stored is flash and SD card""" - from ..encryption import MnemonicStorage - from ..settings import THIN_SPACE - - mnemonic_ids_menu = [] - mnemonic_storage = MnemonicStorage() - mnemonics = mnemonic_storage.list_mnemonics() - sd_mnemonics = mnemonic_storage.list_mnemonics(sd_card=True) - del mnemonic_storage - - for mnemonic_id in sorted(mnemonics): - mnemonic_ids_menu.append( - ( - mnemonic_id + " (flash)", - lambda m_id=mnemonic_id: ( - self._remove_encrypted_mnemonic(m_id) - if remove_opt - else self._load_encrypted_mnemonic(m_id) - ), - ) - ) - for mnemonic_id in sorted(sd_mnemonics): - mnemonic_ids_menu.append( - ( - mnemonic_id + " (SD" + THIN_SPACE + "card)", - lambda m_id=mnemonic_id: ( - self._remove_encrypted_mnemonic(m_id, sd_card=True) - if remove_opt - else self._load_encrypted_mnemonic(m_id, sd_card=True) - ), - ) - ) - submenu = Menu(self.ctx, mnemonic_ids_menu) - index, status = submenu.run_loop() - if index == submenu.back_index: - return MENU_CONTINUE - return status - - def _load_encrypted_mnemonic(self, mnemonic_id, sd_card=False): - """Uses encryption module to load and decrypt a mnemonic""" - from ..encryption import MnemonicStorage - - error_txt = t("Failed to decrypt") - - key_capture = EncryptionKey(self.ctx) - key = key_capture.encryption_key() - if key in (None, "", ESC_KEY): - self.flash_error(t("Key was not provided")) - return MENU_CONTINUE - self.ctx.display.clear() - self.ctx.display.draw_centered_text(t("Processing…")) - mnemonic_storage = MnemonicStorage() - try: - words = mnemonic_storage.decrypt(key, mnemonic_id, sd_card).split() - except: - self.flash_error(error_txt) - return MENU_CONTINUE - - if len(words) not in (12, 24): - self.flash_error(error_txt) - return MENU_CONTINUE - del mnemonic_storage - return words - - def _remove_encrypted_mnemonic(self, mnemonic_id, sd_card=False): - """Deletes a mnemonic""" - from ..encryption import MnemonicStorage - - mnemonic_storage = MnemonicStorage() - self.ctx.display.clear() - if self.prompt(t("Remove %s?") % mnemonic_id, self.ctx.display.height() // 2): - mnemonic_storage.del_mnemonic(mnemonic_id, sd_card) - message = t("%s removed.") % mnemonic_id - message += "\n\n" - if sd_card: - message += t( - "Fully erase your SD card in another device to ensure data is unrecoverable" - ) - else: - message += t("To ensure data is unrecoverable use Wipe Device feature") - self.ctx.display.clear() - self.ctx.display.draw_centered_text(message) - self.ctx.input.wait_for_button() - del mnemonic_storage diff --git a/REFERENCE/kef.py b/REFERENCE/kef.py deleted file mode 100644 index 42aab1a..0000000 --- a/REFERENCE/kef.py +++ /dev/null @@ -1,564 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021-2025 Krux contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import ucryptolib -import uhashlib_hw - - -# KEF: AES, MODEs VERSIONS, MODE_NUMBERS, and MODE_IVS are defined here -# to disable a MODE: set its value to None -# to disable a VERSION: set its value to None -AES = ucryptolib.aes -MODE_ECB = ucryptolib.MODE_ECB -MODE_CBC = ucryptolib.MODE_CBC -MODE_CTR = ucryptolib.MODE_CTR -MODE_GCM = ucryptolib.MODE_GCM -VERSIONS = { - # initial versions: released 2023.08 to encrypt bip39 entropy bytes - 0: { - "name": "AES-ECB v1", - "mode": MODE_ECB, - "auth": -16, - }, - 1: { - "name": "AES-CBC v1", - "mode": MODE_CBC, - "auth": -16, - }, - # AES in ECB mode - 5: { - # smallest ECB ciphertext, w/ unsafe padding: for high entropy mnemonics, passphrases, etc - "name": "AES-ECB", - "mode": MODE_ECB, - "auth": 3, - }, - 6: { - # safe padding: for mid-sized plaintext w/o duplicate blocks - "name": "AES-ECB +p", - "mode": MODE_ECB, - "pkcs_pad": True, - "auth": -4, - }, - 7: { - # compressed, w/ safe padding: for larger plaintext; may compact otherwise duplicate blocks - "name": "AES-ECB +c", - "mode": MODE_ECB, - "pkcs_pad": True, - "auth": -4, - "compress": True, - }, - # AES in CBC mode - 10: { - # smallest CBC cipherext, w/ unsafe padding: for mnemonics, passphrases, etc - "name": "AES-CBC", - "mode": MODE_CBC, - "auth": 4, - }, - 11: { - # safe padding: for mid-sized plaintext - "name": "AES-CBC +p", - "mode": MODE_CBC, - "pkcs_pad": True, - "auth": -4, - }, - 12: { - # compressed, w/ safe padding: for larger plaintext - "name": "AES-CBC +c", - "mode": MODE_CBC, - "pkcs_pad": True, - "auth": -4, - "compress": True, - }, - # AES in CTR stream mode - 15: { - # doesn't require padding: for small and mid-sized plaintext - "name": "AES-CTR", - "mode": MODE_CTR, - "pkcs_pad": None, - "auth": -4, - }, - 16: { - # compressed: for larger plaintext - "name": "AES-CTR +c", - "mode": MODE_CTR, - "pkcs_pad": None, - "auth": -4, - "compress": True, - }, - # AES in GCM stream mode - 20: { - # doesn't require padding: for small and mid-sized plaintext - "name": "AES-GCM", - "mode": MODE_GCM, - "pkcs_pad": None, - "auth": 4, - }, - 21: { - # compressed: for larger plaintext - "name": "AES-GCM +c", - "mode": MODE_GCM, - "pkcs_pad": None, - "auth": 4, - "compress": True, - }, -} -MODE_NUMBERS = { - "AES-ECB": MODE_ECB, - "AES-CBC": MODE_CBC, - "AES-CTR": MODE_CTR, - "AES-GCM": MODE_GCM, -} -MODE_IVS = { - MODE_CBC: 16, - MODE_CTR: 12, - MODE_GCM: 12, -} - -AES_BLOCK_SIZE = 16 - - -class Cipher: - """More than just a helper for AES encrypt/decrypt. Enforces KEF VERSIONS rules""" - - def __init__(self, key, salt, iterations): - key = key if isinstance(key, bytes) else key.encode() - salt = salt if isinstance(salt, bytes) else salt.encode() - self._key = uhashlib_hw.pbkdf2_hmac_sha256(key, salt, iterations) - - def encrypt(self, plain, version, iv=b"", fail_unsafe=True): - """AES encrypt according to KEF rules defined by version, returns payload bytes""" - mode = VERSIONS[version]["mode"] - v_iv = MODE_IVS.get(mode, 0) - v_pkcs_pad = VERSIONS[version].get("pkcs_pad", False) - v_auth = VERSIONS[version].get("auth", 0) - v_compress = VERSIONS[version].get("compress", False) - auth = b"" - if iv is None: - iv = b"" - - if not isinstance(plain, bytes): - raise TypeError("Plaintext is not bytes") - - # for versions that compress - if v_compress: - plain = _deflate(plain) - - # fail: post-encryption appended "auth" with unfaithful-padding breaks decryption - if fail_unsafe and v_pkcs_pad is False and v_auth > 0 and plain[-1] == 0x00: - raise ValueError("Cannot validate decryption for this plaintext") - - # for modes that don't have authentication, KEF uses 2 forms of sha256 - if v_auth != 0 and mode in (MODE_ECB, MODE_CBC, MODE_CTR): - if v_auth > 0: - # unencrypted (public) auth: hash the plaintext w/ self._key - auth = uhashlib_hw.sha256( - bytes([version]) + iv + plain + self._key - ).digest()[:v_auth] - elif v_auth < 0: - # encrypted auth: hash only the plaintext - auth = uhashlib_hw.sha256(plain).digest()[:-v_auth] - - # fail: same case as above if auth bytes have NUL suffix - if fail_unsafe and v_pkcs_pad is False and auth[-1] == 0x00: - raise ValueError("Cannot validate decryption for this plaintext") - plain += auth - auth = b"" - - # some modes need to pad to AES 16-byte blocks - if v_pkcs_pad is True or v_pkcs_pad is False: - plain = _pad(plain, pkcs_pad=v_pkcs_pad) - - # fail to encrypt in modes where it is known unsafe - if fail_unsafe and mode == MODE_ECB: - unique_blocks = len( - set((plain[x : x + 16] for x in range(0, len(plain), 16))) - ) - if unique_blocks != len(plain) // 16: - raise ValueError("Duplicate blocks in ECB mode") - - # setup the encryptor (checking for modes that need initialization-vector) - if v_iv > 0: - if not (isinstance(iv, bytes) and len(iv) == v_iv): - raise ValueError("Wrong IV length") - elif iv: - raise ValueError("IV is not required") - if iv: - if mode == MODE_CTR: - encryptor = AES(self._key, mode, nonce=iv) - elif mode == MODE_GCM: - encryptor = AES(self._key, mode, iv, mac_len=v_auth) - else: - encryptor = AES(self._key, mode, iv) - else: - encryptor = AES(self._key, mode) - - # encrypt the plaintext - encrypted = encryptor.encrypt(plain) - - # for modes that do have inherent authentication, use it - if mode == MODE_GCM: - auth = encryptor.digest()[:v_auth] - - return iv + encrypted + auth - - def decrypt(self, payload, version): - """AES Decrypt according to KEF rules defined by version, returns plaintext bytes""" - mode = VERSIONS[version]["mode"] - v_iv = MODE_IVS.get(mode, 0) - v_pkcs_pad = VERSIONS[version].get("pkcs_pad", False) - v_auth = VERSIONS[version].get("auth", 0) - v_compress = VERSIONS[version].get("compress", False) - - # validate payload size early - min_payload = 1 if mode in (MODE_CTR, MODE_GCM) else AES_BLOCK_SIZE - min_payload += min(0, v_auth) + v_iv - if len(payload) <= min_payload: - raise ValueError("Invalid Payload") - - # setup decryptor (pulling initialization-vector from payload if necessary) - if not v_iv: - iv = b"" - decryptor = AES(self._key, mode) - else: - iv = payload[:v_iv] - if mode == MODE_CTR: - decryptor = AES(self._key, mode, nonce=iv) - elif mode == MODE_GCM: - decryptor = AES(self._key, mode, iv, mac_len=v_auth) - else: - decryptor = AES(self._key, mode, iv) - payload = payload[v_iv:] - - # remove authentication from payload if suffixed to ciphertext - auth = None - if v_auth > 0: - auth = payload[-v_auth:] - payload = payload[:-v_auth] - - # decrypt the ciphertext - decrypted = decryptor.decrypt(payload) - - # if authentication added (inherent or added by KEF for ECB/CBC) - # then: unpad and validate via embeded authentication bytes - # else: let caller deal with unpad and auth - if v_auth != 0: - try: - decrypted = self._authenticate( - version, iv, decrypted, decryptor, auth, mode, v_auth, v_pkcs_pad - ) - except: - decrypted = None - - # for versions that compress - if decrypted and v_compress: - decrypted = _reinflate(decrypted) - - return decrypted - - def _authenticate( - self, version, iv, decrypted, aes_object, auth, mode, v_auth, v_pkcs_pad - ): - if not ( - isinstance(version, int) - and 0 <= version <= 255 - and isinstance(iv, bytes) - and isinstance(decrypted, bytes) - and ( - mode != MODE_GCM - or (hasattr(aes_object, "verify") and callable(aes_object.verify)) - ) - and (isinstance(auth, bytes) or auth is None) - and mode in MODE_NUMBERS.values() - and (isinstance(v_auth, int) and -32 <= v_auth <= 32) - and (v_pkcs_pad is True or v_pkcs_pad is False or v_pkcs_pad is None) - ): - raise ValueError("Invalid call to ._authenticate()") - - # some modes need to unpad - len_pre_unpad = len(decrypted) - if v_pkcs_pad in (False, True): - decrypted = _unpad(decrypted, pkcs_pad=v_pkcs_pad) - - if v_auth < 0: - # auth was added to plaintext - auth = decrypted[v_auth:] - decrypted = decrypted[:v_auth] - - # versions that have built-in authentication use their own - if mode == MODE_GCM: - try: - aes_object.verify(auth) - return decrypted - except: - return None - - # versions that don't have built-in authentication use 2 forms of sha256 - max_attempts = 1 - if v_pkcs_pad is False: - # NUL padding is imperfect, still attempt to authenticate -- up to a limit... - # ... lesser of num bytes unpadded and auth size+1, + 1 - max_attempts = min(len_pre_unpad - len(decrypted), abs(v_auth) + 1) + 1 - - for _ in range(max_attempts): - if v_auth > 0: - # for unencrypted (public) auth > 0: hash the decrypted w/ self._key - cksum = uhashlib_hw.sha256( - bytes([version]) + iv + decrypted + self._key - ).digest()[:v_auth] - else: - # for encrypted auth < 0: hash only the decrypted - cksum = uhashlib_hw.sha256(decrypted).digest()[:-v_auth] - if cksum == auth: - return decrypted - - if v_auth < 0: - # for next attempt, assume auth had NUL stripped by unpad() - decrypted += auth[:1] - auth = auth[1:] + b"\x00" - elif v_auth > 0: - # for next attempt, assume plaintext had NUL stripped by unpad() - decrypted += b"\x00" - return None - - -def suggest_versions(plaintext, mode_name): - """Suggests a krux encryption version based on plaintext and preferred mode""" - - small_thresh = 32 # if len(plaintext) <= small_thresh: it is small - big_thresh = 120 # if len(plaintext) >= big_thresh: it is big - - # gather metrics on plaintext - if not isinstance(plaintext, (bytes, str)): - raise TypeError("Plaintext is not bytes or str") - p_length = len(plaintext) - unique_blocks = len(set((plaintext[x : x + 16] for x in range(0, p_length, 16)))) - p_duplicates = bool(unique_blocks < p_length / 16) - if isinstance(plaintext, bytes): - p_nul_suffix = bool(plaintext[-1] == 0x00) - else: - p_nul_suffix = bool(plaintext.encode()[-1] == 0x00) - - candidates = [] - for version, values in VERSIONS.items(): - # strategy: eliminate bad choices of versions - # TODO: explore a strategy that cuts to the best one right away - - if values is None or values["mode"] is None: - continue - - # never use a version that is not the correct mode - if values["mode"] != MODE_NUMBERS[mode_name]: - continue - v_compress = values.get("compress", False) - v_auth = values.get("auth", 0) - v_pkcs_pad = values.get("pkcs_pad", False) - - # never use non-compressed ECB when plaintext has duplicate blocks - if p_duplicates and mode_name == "AES-ECB" and not v_compress: - continue - - # never use v1 versions since v2 is smaller - if mode_name in ("AES-ECB", "AES-CBC") and v_auth == -16: - continue - - # based on plaintext size - if p_length <= small_thresh: - # except unsafe ECB text... - if mode_name == "AES-ECB" and p_duplicates: - pass - else: - # ...never use pkcs when it's small and can keep it small - if v_pkcs_pad is True and not p_nul_suffix: - continue - # ...and never compress - if v_compress: - continue - else: - # never use non-safe padding for not-small plaintext - if v_pkcs_pad is False: - continue - - # except unsafe ECB text... - if mode_name == "AES-ECB" and p_duplicates: - pass - elif p_length < big_thresh: - # ...never use compressed for not-big plaintext - if v_compress: - continue - else: - # never use non-compressed for big plaintext - if not v_compress: - continue - - # never use a version with unsafe padding if plaintext ends 0x00 - if p_nul_suffix and v_pkcs_pad is False: - continue - - candidates.append(version) - return candidates - - -def wrap(id_, version, iterations, payload): - """ - Wraps inputs into KEF Encryption Format envelope, returns bytes - """ - - try: - # when wrapping, be tolerant about id_ as bytes or str - id_ = id_ if isinstance(id_, bytes) else id_.encode() - if not 0 <= len(id_) <= 252: - raise ValueError - len_id = len(id_).to_bytes(1, "big") - except: - raise ValueError("Invalid ID") - - try: - if not ( - 0 <= version <= 255 - and VERSIONS[version] is not None - and VERSIONS[version]["mode"] is not None - ): - raise ValueError - except: - raise ValueError("Invalid version") - - try: - if not isinstance(iterations, int): - raise ValueError - if iterations % 10000 == 0: - iterations = iterations // 10000 - if not 1 <= iterations <= 10000: - raise ValueError - else: - if not 10000 < iterations < 2**24: - raise ValueError - iterations = iterations.to_bytes(3, "big") - except: - raise ValueError("Invalid iterations") - - extra = MODE_IVS.get(VERSIONS[version]["mode"], 0) - if VERSIONS[version].get("auth", 0) > 0: - extra += VERSIONS[version]["auth"] - if not isinstance(payload, bytes): - raise ValueError("Payload is not bytes") - if VERSIONS[version].get("pkcs_pad", False) in (True, False): - if (len(payload) - extra) % 16 != 0: - raise ValueError("Ciphertext is not aligned") - if (len(payload) - extra) // 16 < 1: - raise ValueError("Ciphertext is too short") - - version = version.to_bytes(1, "big") - return b"".join([len_id, id_, version, iterations, payload]) - - -def unwrap(kef_bytes): - """ - Unwraps KEF Encryption Format bytes, returns tuple of parsed values - """ - len_id = kef_bytes[0] - - try: - # out-of-order reading to validate version early - version = kef_bytes[1 + len_id] - if VERSIONS[version] is None or VERSIONS[version]["mode"] is None: - raise ValueError - except: - raise ValueError("Invalid format") - - # When unwrapping, be strict returning id_ as bytes - id_ = kef_bytes[1 : 1 + len_id] - - kef_iterations = int.from_bytes(kef_bytes[2 + len_id : 5 + len_id], "big") - if kef_iterations <= 10000: - iterations = kef_iterations * 10000 - else: - iterations = kef_iterations - - payload = kef_bytes[len_id + 5 :] - extra = MODE_IVS.get(VERSIONS[version]["mode"], 0) - if VERSIONS[version].get("auth", 0) > 0: - extra += VERSIONS[version]["auth"] - if VERSIONS[version].get("pkcs_pad", False) in (True, False): - if (len(payload) - extra) % 16 != 0: - raise ValueError("Ciphertext is not aligned") - if (len(payload) - extra) // 16 < 1: - raise ValueError("Ciphertext is too short") - - return (id_, version, iterations, payload) - - -def _pad(some_bytes, pkcs_pad): - """ - Pads some_bytes to AES block size of 16 bytes, returns bytes - pkcs_pad: False=NUL-pad, True=PKCS#7-pad, None=no-pad - """ - if pkcs_pad is None: - return some_bytes - len_padding = (AES_BLOCK_SIZE - len(some_bytes) % AES_BLOCK_SIZE) % AES_BLOCK_SIZE - if pkcs_pad is True: - if len_padding == 0: - len_padding = AES_BLOCK_SIZE - return some_bytes + (len_padding).to_bytes(1, "big") * len_padding - if pkcs_pad is False: - return some_bytes + b"\x00" * len_padding - raise TypeError("pkcs_pad is not (None, True, False)") - - -def _unpad(some_bytes, pkcs_pad): - """ - Strips padding from some_bytes, returns bytes - pkcs_pad: False=NUL-pad, True=PKCS#7-pad, None=no-pad - """ - if pkcs_pad is None: - return some_bytes - if pkcs_pad is True: - len_padding = some_bytes[-1] - return some_bytes[:-len_padding] - if pkcs_pad is False: - return some_bytes.rstrip(b"\x00") - raise TypeError("pkcs_pad is not in (None, True, False)") - - -def _deflate(data): - """Compresses the given data using deflate module""" - import io - import deflate - - try: - stream = io.BytesIO() - with deflate.DeflateIO(stream) as d: - d.write(data) - return stream.getvalue() - except: - raise ValueError("Error compressing") - - -def _reinflate(data): - """Decompresses the given data using deflate module""" - import io - import deflate - - try: - with deflate.DeflateIO(io.BytesIO(data)) as d: - return d.read() - except: - raise ValueError("Error decompressing") diff --git a/REFERENCE/krux b/REFERENCE/krux deleted file mode 160000 index bff65f4..0000000 --- a/REFERENCE/krux +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bff65f4fb8bf8a52551f28179a624be5e9765e49 diff --git a/REFERENCE/krux-test/.gitignore b/REFERENCE/krux-test/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/REFERENCE/krux-test/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/REFERENCE/krux-test/README.md b/REFERENCE/krux-test/README.md deleted file mode 100644 index f848e66..0000000 --- a/REFERENCE/krux-test/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# krux-test - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run -``` - -This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/REFERENCE/krux-test/bun.lock b/REFERENCE/krux-test/bun.lock deleted file mode 100644 index e899d70..0000000 --- a/REFERENCE/krux-test/bun.lock +++ /dev/null @@ -1,33 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "krux-test", - "dependencies": { - "bip39": "^3.1.0", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], - - "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], - - "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "bip39": ["bip39@3.1.0", "", { "dependencies": { "@noble/hashes": "^1.2.0" } }, "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A=="], - - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - } -} diff --git a/REFERENCE/krux-test/krux-test.ts b/REFERENCE/krux-test/krux-test.ts deleted file mode 100644 index e21a8a4..0000000 --- a/REFERENCE/krux-test/krux-test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as bip39 from "bip39"; -// Bun implements the Web Crypto API globally as `crypto` - -const BASE43_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:"; - -// --- Helper: Base43 Decode --- -function base43Decode(str: string): Uint8Array { - let value = 0n; - const base = 43n; - - for (const char of str) { - const index = BASE43_ALPHABET.indexOf(char); - if (index === -1) throw new Error(`Invalid Base43 char: ${char}`); - value = value * base + BigInt(index); - } - - // Convert BigInt to Buffer/Uint8Array - let hex = value.toString(16); - if (hex.length % 2 !== 0) hex = '0' + hex; - return new Uint8Array(Buffer.from(hex, 'hex')); -} - -// --- Main Decryption Function --- -async function decryptKruxKEF(kefData: string, passphrase: string) { - // 1. Decode Base43 - const rawBytes = base43Decode(kefData); - - // 2. Parse Envelope - let offset = 0; - - // ID Length (1 byte) - const idLen = rawBytes[offset++]; - - // ID / Salt - const salt = rawBytes.slice(offset, offset + idLen); - offset += idLen; - - // Version (1 byte) - const version = rawBytes[offset++]; - - // Iterations (3 bytes, Big-Endian) - const iterBytes = rawBytes.slice(offset, offset + 3); - const iterations = (iterBytes[0] << 16) | (iterBytes[1] << 8) | iterBytes[2]; - offset += 3; - - // Payload: [IV (12 bytes)] [Ciphertext (N)] [Tag (4 bytes)] - const payload = rawBytes.slice(offset); - const iv = payload.slice(0, 12); - const tagLength = 4; - const ciphertextWithTag = payload.slice(12); - - console.log("--- Parsed KEF Data ---"); - console.log(`Version: ${version}`); - console.log(`Iterations: ${iterations}`); - console.log(`Salt (hex): ${Buffer.from(salt).toString('hex')}`); - - if (version !== 20) { - throw new Error("Only KEF Version 20 (AES-GCM) is supported."); - } - - // 3. Derive Key (PBKDF2-HMAC-SHA256) - const keyMaterial = await crypto.subtle.importKey( - "raw", - new TextEncoder().encode(passphrase), - { name: "PBKDF2" }, - false, - ["deriveKey"] - ); - - const key = await crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt: salt, - iterations: iterations, - hash: "SHA-256", - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["decrypt"] - ); - - // 4. Decrypt (AES-GCM) - try { - const decryptedBuffer = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv: iv, - tagLength: 32, // 4 bytes * 8 - }, - key, - ciphertextWithTag - ); - - return new Uint8Array(decryptedBuffer); - - } catch (error) { - throw new Error(`Decryption failed: ${error}`); - } -} - -// --- Run Test --- -const kefString = "1334+HGXM$F8PPOIRNHX0.R*:SBMHK$X88LX$*/Y417R/6S1ZQOB2LHM-L+4T1YQVU:B*CKGXONP7:Y/R-B*:$R8FK"; -const passphrase = "aaa"; -const expectedMnemonic = "differ release beauty fresh tortoise usage curtain spoil october town embrace ridge rough reject cabin snap glimpse enter book coach green lonely hundred mercy"; - -console.log(`\nDecrypting KEF String...`); -try { - const entropy = await decryptKruxKEF(kefString, passphrase); - const mnemonic = bip39.entropyToMnemonic(Buffer.from(entropy)); - - console.log("\n--- Result ---"); - console.log(`Mnemonic: ${mnemonic}`); - - if (mnemonic === expectedMnemonic) { - console.log("\n✅ SUCCESS: Mnemonic matches expected output."); - } else { - console.log("\n❌ FAIL: Mnemonic does not match."); - } -} catch (e) { - console.error(e); -} - diff --git a/REFERENCE/krux-test/package.json b/REFERENCE/krux-test/package.json deleted file mode 100644 index 1399ed2..0000000 --- a/REFERENCE/krux-test/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "krux-test", - "private": true, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "bip39": "^3.1.0" - } -} diff --git a/REFERENCE/krux-test/tsconfig.json b/REFERENCE/krux-test/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/REFERENCE/krux-test/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/REFERENCE/qr.py b/REFERENCE/qr.py deleted file mode 100644 index 34845e6..0000000 --- a/REFERENCE/qr.py +++ /dev/null @@ -1,403 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021-2024 Krux contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# pylint: disable=E1101 -import io -import math -import qrcode - -FORMAT_NONE = 0 -FORMAT_PMOFN = 1 -FORMAT_UR = 2 -FORMAT_BBQR = 3 - -PMOFN_PREFIX_LENGTH_1D = 6 -PMOFN_PREFIX_LENGTH_2D = 8 -BBQR_PREFIX_LENGTH = 8 -UR_GENERIC_PREFIX_LENGTH = 22 - -# CBOR_PREFIX = 6 bytes for tags, 1 for index, 1 for max_index, 2 for message len, 4 for checksum -# Check UR's fountain_encoder.py file, on Part.cbor() function for more details -UR_CBOR_PREFIX_LEN = 14 -UR_BYTEWORDS_CRC_LEN = 4 # 32 bits CRC used on Bytewords encoding - -UR_MIN_FRAGMENT_LENGTH = 10 - -# https://www.qrcode.com/en/about/version.html -# List of capacities, based on versions -# Tables below are limited to version 20 and we use L (Low) ECC (Error Correction Code) Level - -# [0-9] = 10 chars -# Version 1(index 0)=21x21px = 41 chars, version 2=25x25px = 77 chars ... -QR_CAPACITY_NUMERIC = [ - 41, - 77, - 127, - 187, - 255, - 322, - 370, - 461, - 552, - 652, - 772, - 883, - 1022, - 1101, - 1250, - 1408, - 1548, - 1725, - 1903, - 2061, -] - - -# [A-Z0-9 $%*+\-./:] = 45 chars (no lowercase!) -# Version 1(index 0)=21x21px = 25 chars, version 2=25x25px = 47 chars ... -QR_CAPACITY_ALPHANUMERIC = [ - 25, - 47, - 77, - 114, - 154, - 195, - 224, - 279, - 335, - 395, - 468, - 535, - 619, - 667, - 758, - 854, - 938, - 1046, - 1153, - 1249, -] - - -# ASCII, UTF-8 (any 8-bit / 1 byte sequence) -# Requires more pixels to show information -# Version 1(index 0)=21x21px = 17 bytes, version 2=25x25px = 32 bytes ... -QR_CAPACITY_BYTE = [ - 17, - 32, - 53, - 78, - 106, - 134, - 154, - 192, - 230, - 271, - 321, - 367, - 425, - 458, - 520, - 586, - 644, - 718, - 792, - 858, -] - - -class QRPartParser: - """Responsible for parsing either a singular or animated series of QR codes - and returning the final decoded, combined data - """ - - def __init__(self): - self.parts = {} - self.total = -1 - self.format = None - self.decoder = None - self.bbqr = None - - def parsed_count(self): - """Returns the number of parsed parts so far""" - if self.format == FORMAT_UR: - # Single-part URs have no expected part indexes - if self.decoder.fountain_decoder.expected_part_indexes is None: - return 1 if self.decoder.result is not None else 0 - completion_pct = self.decoder.estimated_percent_complete() - return math.ceil(completion_pct * self.total_count() / 2) + len( - self.decoder.fountain_decoder.received_part_indexes - ) - return len(self.parts) - - def processed_parts_count(self): - """Returns quantity of processed QR code parts""" - if self.format == FORMAT_UR: - return self.decoder.fountain_decoder.processed_parts_count - return len(self.parts) - - def total_count(self): - """Returns the total number of parts there should be""" - if self.format == FORMAT_UR: - # Single-part URs have no expected part indexes - if self.decoder.fountain_decoder.expected_part_indexes is None: - return 1 - return self.decoder.expected_part_count() * 2 - return self.total - - def parse(self, data): - """Parses the QR data, extracting part information""" - if self.format is None: - self.format, self.bbqr = detect_format(data) - - if self.format == FORMAT_NONE: - self.parts[1] = data - self.total = 1 - elif self.format == FORMAT_PMOFN: - part, index, total = parse_pmofn_qr_part(data) - self.parts[index] = part - self.total = total - return index - 1 - elif self.format == FORMAT_UR: - if not self.decoder: - from ur.ur_decoder import URDecoder - - self.decoder = URDecoder() - self.decoder.receive_part(data) - elif self.format == FORMAT_BBQR: - from .bbqr import parse_bbqr - - part, index, total = parse_bbqr(data) - self.parts[index] = part - self.total = total - return index - return None - - def is_complete(self): - """Returns a boolean indicating whether or not enough parts have been parsed""" - if self.format == FORMAT_UR: - return self.decoder.is_complete() - keys_check = ( - sum(range(1, self.total + 1)) - if self.format in (FORMAT_PMOFN, FORMAT_NONE) - else sum(range(self.total)) - ) - return ( - self.total != -1 - and self.parsed_count() == self.total_count() - and sum(self.parts.keys()) == keys_check - ) - - def result(self): - """Returns the combined part data""" - if self.format == FORMAT_UR: - from ur.ur import UR - - return UR(self.decoder.result.type, bytearray(self.decoder.result.cbor)) - - if self.format == FORMAT_BBQR: - from .bbqr import decode_bbqr - - return decode_bbqr(self.parts, self.bbqr.encoding, self.bbqr.file_type) - - code_buffer = io.StringIO("") - for _, part in sorted(self.parts.items()): - if isinstance(part, bytes): - # Encoded data won't write on StringIO - return part - code_buffer.write(part) - code = code_buffer.getvalue() - code_buffer.close() - return code - - -def to_qr_codes(data, max_width, qr_format): - """Returns the list of QR codes necessary to represent the data in the qr format, given - the max_width constraint - """ - if qr_format == FORMAT_NONE: - code = qrcode.encode(data) - yield (code, 1) - else: - num_parts, part_size = find_min_num_parts(data, max_width, qr_format) - if qr_format == FORMAT_PMOFN: - part_index = 0 - while True: - part_number = "p%dof%d " % (part_index + 1, num_parts) - if isinstance(data, bytes): - part_number = part_number.encode() - part = None - if part_index == num_parts - 1: - part = part_number + data[part_index * part_size :] - part_index = 0 - else: - part = ( - part_number - + data[part_index * part_size : (part_index + 1) * part_size] - ) - part_index += 1 - code = qrcode.encode(part) - yield (code, num_parts) - elif qr_format == FORMAT_UR: - from ur.ur_encoder import UREncoder - - encoder = UREncoder(data, part_size, 0) - while True: - part = encoder.next_part() - code = qrcode.encode(part) - yield (code, encoder.fountain_encoder.seq_len()) - elif qr_format == FORMAT_BBQR: - from .bbqr import int2base36 - - part_index = 0 - while True: - header = "B$%s%s%s%s" % ( - data.encoding, - data.file_type, - int2base36(num_parts), - int2base36(part_index), - ) - part = None - if part_index == num_parts - 1: - part = header + data.payload[part_index * part_size :] - part_index = 0 - else: - part = ( - header - + data.payload[ - part_index * part_size : (part_index + 1) * part_size - ] - ) - part_index += 1 - code = qrcode.encode(part) - yield (code, num_parts) - - -def get_size(qr_code): - """Returns the size of the qr code as the number of chars until the first newline""" - size = math.sqrt(len(qr_code) * 8) - return int(size) - - -def max_qr_bytes(max_width, encoding="byte"): - """Calculates the maximum length, in bytes, a QR code of a given size can store""" - # Given qr_size = 17 + 4 * version + 2 * frame_size - max_width -= 2 # Subtract frame width - qr_version = (max_width - 17) // 4 - if encoding == "alphanumeric": - capacity_list = QR_CAPACITY_ALPHANUMERIC - else: - capacity_list = QR_CAPACITY_BYTE - - try: - return capacity_list[qr_version - 1] - except: - # Limited to version 20 - return capacity_list[-1] - - -def find_min_num_parts(data, max_width, qr_format): - """Finds the minimum number of QR parts necessary to encode the data in - the specified format within the max_width constraint - """ - encoding = "alphanumeric" if qr_format == FORMAT_BBQR else "byte" - qr_capacity = max_qr_bytes(max_width, encoding) - if qr_format == FORMAT_PMOFN: - data_length = len(data) - part_size = qr_capacity - PMOFN_PREFIX_LENGTH_1D - # where prefix = "pXofY " where Y < 9 - num_parts = (data_length + part_size - 1) // part_size - if num_parts > 9: # Prefix has 2 digits numbers, so re-calculate - part_size = qr_capacity - PMOFN_PREFIX_LENGTH_2D - # where prefix = "pXXofYY " where max YY = 99 - num_parts = (data_length + part_size - 1) // part_size - part_size = (data_length + num_parts - 1) // num_parts - elif qr_format == FORMAT_UR: - qr_capacity -= ( - # This is an approximation, UR index grows indefinitely - UR_GENERIC_PREFIX_LENGTH # index: ~ "ur:crypto-psbt/xxx-xx/" - ) - # UR will add a bunch of info (some duplicated) on the body of each QR - # Info's lenght is multiplied by 2 in Bytewords.encode step - qr_capacity -= (UR_CBOR_PREFIX_LEN + UR_BYTEWORDS_CRC_LEN) * 2 - qr_capacity = max(UR_MIN_FRAGMENT_LENGTH, qr_capacity) - data_length = len(data.cbor) - data_length *= 2 # UR will Bytewords.encode, which multiply bytes length by 2 - num_parts = (data_length + qr_capacity - 1) // qr_capacity - # For UR, part size will be the input for "max_fragment_len" - part_size = len(data.cbor) // num_parts - part_size = max(part_size, UR_MIN_FRAGMENT_LENGTH) - # UR won't use "num_parts", will use encoder.fountain_encoder.seq_len() instead - elif qr_format == FORMAT_BBQR: - data_length = len(data.payload) - max_part_size = qr_capacity - BBQR_PREFIX_LENGTH - if data_length < max_part_size: - return 1, data_length - # Round max_part_size to the nearest lower multiple of 8 - max_part_size = (max_part_size // 8) * 8 - # Calculate the number of parts required (rounded up) - num_parts = (data_length + max_part_size - 1) // max_part_size - # Calculate the optimal part size - part_size = data_length // num_parts - # Round to the nearest higher multiple of 8 - part_size = ((part_size + 7) // 8) * 8 - # Check if the part size is within the limits - if part_size > max_part_size: - num_parts += 1 - part_size = data_length // num_parts - # Round to the nearest higher multiple of 8 again - part_size = ((part_size + 7) // 8) * 8 - else: - raise ValueError("Invalid format type") - return num_parts, part_size - - -def parse_pmofn_qr_part(data): - """Parses the QR as a P M-of-N part, extracting the part's content, index, and total""" - of_index = data.index("of") - space_index = data.index(" ") - part_index = int(data[1:of_index]) - part_total = int(data[of_index + 2 : space_index]) - return data[space_index + 1 :], part_index, part_total - - -def detect_format(data): - """Detects the QR format of the given data""" - qr_format = FORMAT_NONE - try: - if data.startswith("p"): - header = data.split(" ")[0] - if "of" in header and header[1:].split("of")[0].isdigit(): - qr_format = FORMAT_PMOFN - elif data.lower().startswith("ur:"): - qr_format = FORMAT_UR - elif data.startswith("B$"): - from .bbqr import BBQrCode, KNOWN_ENCODINGS, KNOWN_FILETYPES - - if data[3] in KNOWN_FILETYPES: - bbqr_file_type = data[3] - if data[2] in KNOWN_ENCODINGS: - bbqr_encoding = data[2] - return FORMAT_BBQR, BBQrCode(None, bbqr_encoding, bbqr_file_type) - - except: - pass - return qr_format, None diff --git a/REFERENCE/seeds-blender b/REFERENCE/seeds-blender deleted file mode 160000 index 79ceb56..0000000 --- a/REFERENCE/seeds-blender +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 79ceb564f5e39eff4f81c12bcf9b5d67c959658b