This commit is contained in:
LC mac
2026-02-07 04:24:57 +08:00
parent f4538b9b6c
commit a021044a19
22 changed files with 0 additions and 3350 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

View File

@@ -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()

View File

@@ -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

View File

@@ -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: <img src="\cf4 \strokec4 $\{qrDataURL.slice(\cf8 \strokec8 0\cf4 \strokec4 , \cf8 \strokec8 50\cf4 \strokec4 )\}\cf6 \strokec6 ..." />`\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}}\
}

View File

@@ -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!\
}

View File

@@ -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

View File

@@ -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")

Submodule REFERENCE/krux deleted from bff65f4fb8

View File

@@ -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

View File

@@ -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.

View File

@@ -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=="],
}
}

View File

@@ -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);
}

View File

@@ -1,13 +0,0 @@
{
"name": "krux-test",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"bip39": "^3.1.0"
}
}

View File

@@ -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
}
}

View File

@@ -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

Submodule REFERENCE/seeds-blender deleted from 79ceb564f5