clean up
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 209 KiB |
@@ -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()
|
||||
@@ -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
|
||||
@@ -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}}\
|
||||
}
|
||||
@@ -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!\
|
||||
}
|
||||
@@ -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
|
||||
564
REFERENCE/kef.py
@@ -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")
|
||||
34
REFERENCE/krux-test/.gitignore
vendored
@@ -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
|
||||
@@ -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.
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "krux-test",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"bip39": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
403
REFERENCE/qr.py
@@ -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
|
||||