wordlist-paper/xor.py
scgbckbone 4aea821c2c BUT
2023-03-20 14:45:46 -04:00

201 lines
5.8 KiB
Python

# print some charts
from hashlib import sha256
from mnemonic import Mnemonic
from random import Random
from textwrap import TextWrapper
wordlist = Mnemonic('english').wordlist
WIDTH = 110
def xor_table(indent=' '*8):
print(indent + '| XOR |' + '|'.join(' %X ' % x for x in range(16)))
print(indent + '|----:' + '|---'*16)
for y in range(16):
print(indent + '|**%X**|' % y, end='')
print('|'.join(' %X ' % (x^y) for x in range(16)))
print('''\
- XOR = EOR = ⊕ = Exclusive OR [Wikipedia](https://en.wikipedia.org/wiki/Exclusive_or)
- values in table are: x ⊕ y in hex
- go sideways for first digit, then look down for second digit
- in fact, doesn't matter if you do row or column first
- example: 2 XOR 6 => 4 same as 6 XOR 2 => 4
- any values XOR itself is zero (diagonal on this table)
- alternative view: (x) XOR (y) = flip bits of (x) that are set in (y)
- XOR with zero does nothing (flips no bits)
- XOR with 0xF flips all four bits
- XOR with self flips all set bits, so gives zero
- to XOR three values together, do (a⊕b)=X then (X⊕c)=answer
- right to A, down to B ... take that number, and go to that column
- down to C, that is answer: a ⊕ b ⊕ c
''')
def get_words(entropy):
e_len = len(entropy)
assert e_len in [16, 20, 24, 28, 32]
csum_len = int(e_len / 4)
num_words = int(((e_len * 8) + csum_len) / 11)
v = int.from_bytes(entropy, 'big') << csum_len
indexes = []
for i in range(num_words):
v, m = divmod(v, 2048)
indexes.insert(0, m)
# final csum_len bits are a checksum
indexes[-1] += sha256(entropy).digest()[0] >> (8 - csum_len)
return indexes
def calc_check(words):
words_len = len(words)
csum_len = int(words_len / 3)
e_len = int(((words_len * 11) - csum_len) / 8)
x = 0
for i in range(words_len):
x <<= 11
x |= words[i]
x >>= csum_len
raw = x.to_bytes(e_len, 'big')
cb = sha256(raw).digest()[0] >> (8 - csum_len)
x <<= csum_len
x |= cb
return raw, (x % 0x800)
def entropy_length_to_word_length(entropy_len):
entropy_bit_len = entropy_len * 8
cs_bit_len = entropy_bit_len // 32
return (entropy_bit_len + cs_bit_len) // 11
def print_phrase(raw, indent=' '*4, from_entropy=False):
tw = TextWrapper(width=WIDTH, initial_indent=indent, subsequent_indent=indent)
if from_entropy:
words = get_words(raw)
else:
words = list(raw)
x = ['%d=%s_[%03X]'%(n+1, wordlist[i],i) for n,i in enumerate(words)]
msg = ', '.join(x)
eng = [ln.replace('_', ' ') for ln in tw.wrap(msg)]
hx = ' '.join('%03X'%i for i in words)
return eng, hx, words
def worked_example(count=3, entropy_bytes=32):
rng = Random(123 * count)
num_words = entropy_length_to_word_length(entropy_bytes)
print(f'# {num_words} Words XOR Seed Example Using {count} Parts\n')
indent = ' '*6
digits = []
for n in range(count):
raw = bytearray(rng.randint(0, 255) for _ in range(entropy_bytes))
eng, hx, words = print_phrase(raw, indent, from_entropy=True)
print(f'## Seed {n+10:X} ({n+1} of {count})\n')
print('\n'.join(eng))
print(f'\n{indent}{n+10:X} = {hx}\n\n')
digits.append(hx)
print('## Calculation (XOR each hex digit)\n')
for n in range(count):
print(f'{indent}{n+10:X} = {digits[n]}')
if entropy_bytes == 32:
constant = 2
elif entropy_bytes == 16:
constant = 1
else:
constant = 0
xor = ''
for pos in range(len(digits[0]) - constant):
if digits[0][pos] == ' ':
xor += ' '
continue
here = 0
for n in range(count):
here ^= int(digits[n][pos], 16)
assert 0 <= here < 16, here
xor += '%X' % here
xor += 'x' * constant
lst = list(range(0, 100, 16)) + [23*4]
align = ''.join(('|' if n in lst else ' ') for n in range(len(xor)))
print(f'{indent} {align}')
print(f'{indent[2:]}XOR = {xor}')
print('\n\n## Resulting Seed Phrase\n')
rw = [int(i, 16) for i in xor.split(' ')[:-1]]
eng, hx, _ = print_phrase(rw, indent)
print('\n'.join(eng))
if len(rw) == 11:
_range = 0x10
chk = int(xor.split(' ')[-1][0:2], 16) * _range
elif len(rw) == 23:
_range = 0x100
chk = int(xor.split(' ')[-1][0], 16) * _range
else:
# for these cases - bitwise operations required
# as their checksums have length in (5,6,7)
lw = xor.split(' ')[-1]
lw_int = int(lw, 16)
chk_len = int((len(rw) + 1) / 3)
_range = (2 ** chk_len) - 1
chk = (lw_int >> chk_len) << chk_len
inclusive_range = (chk+_range) - 1
print(f'\n{indent}final word between: %s [%03X] - %s [%03X]' % (
wordlist[chk], chk, wordlist[inclusive_range], inclusive_range))
secret, final = calc_check(rw + [chk])
print(f'{indent}correct final word: %s [%03X]' % (wordlist[final], final))
# check our checksum math
_, chk_hx, _ = print_phrase(secret, indent, from_entropy=True)
assert chk_hx.split(' ')[-1] == '%03X'%final
def footer_text():
print('''\
- It's not possible to calculate the checksum of the final seed phrase on paper (needs SHA256).
- But it must start with the indicated digit(s). If using 24 words XOR, there will be only one
suitable choice offered by the Coldcard in that range (x00 to xFF),
once you have entered the other 23 words.
- The checksum of each of the XOR-parts protects the final result, assuming your XOR
math is correct.
''')
if 1:
print('## XOR Lookup Table\n\n')
xor_table('')
print('---\n')
if 1:
worked_example(3, entropy_bytes=32)
print("\n\n")
worked_example(3, entropy_bytes=16)
print("\n\n")
footer_text()