Compare commits

...

5 Commits

Author SHA1 Message Date
Peter D. Gray
9afdc55858
tweak 2023-03-20 14:58:18 -04:00
scgbckbone
4aea821c2c BUT 2023-03-20 14:45:46 -04:00
scgbckbone
025acf0a24 explicit word length in templates title 2023-03-20 14:45:46 -04:00
scgbckbone
bff7197419 inclusive ranges 2023-03-20 14:45:46 -04:00
scgbckbone
50fa6f6e9b add 12 words XOR 2023-03-20 14:45:46 -04:00
6 changed files with 229 additions and 51 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
*.DS_Store
*.DS_Store
ENV
.idea

View File

@ -36,7 +36,7 @@ def left_label(x):
def top_label(x):
return Paragraph(x, labelStyleCenter)
def doit(fname='worksheet.pdf'):
def doit(fname='worksheet.pdf', word_length=24):
doc = SimpleDocTemplate(fname, pagesize=landscape(letter))
doc.leftMargin = doc.rightMargin = \
@ -44,10 +44,10 @@ def doit(fname='worksheet.pdf'):
# container for the 'Flowable' objects
data = []
blanks = ['' for i in range(24*3)]
blanks = ['' for _ in range(word_length*3)]
for part in 'ABCD':
row = [part, 'Word #']
for i in range(24):
for i in range(word_length):
row.append(str(i+1))
row.append('')
row.append('')
@ -63,7 +63,12 @@ def doit(fname='worksheet.pdf'):
xor = '(A⊕B)⊕C'
else:
xor = '...⊕' + part
data.append([xor, ''] + blanks[:-2] + ['X', 'X'])
if word_length == 24:
data.append([xor, ''] + blanks[:-2] + ['X', 'X'])
elif word_length == 12:
data.append([xor, ''] + blanks[:-1] + ['X'])
else:
data.append([xor, ''] + blanks)
conf = [
#('BACKGROUND',(1,1),(-2,-2),colors.green),
@ -94,14 +99,14 @@ def doit(fname='worksheet.pdf'):
('LINEABOVE', (0,y+3), (-1, y+3), 1.0, colors.grey),
])
for x in range(24):
for x in range(word_length):
pos = 2 + (x*3)
conf.extend([
('SPAN', (pos,y), (pos+2, y)),
('SPAN', (pos,y+1), (pos+2, y+1)),
])
for x in range(3, 24*3, 6):
for x in range(3, word_length*3, 6):
conf.extend([
('BACKGROUND', (2+x,0), (2+x+2, -1), colors.lightgrey),
])
@ -110,7 +115,7 @@ def doit(fname='worksheet.pdf'):
W1 = 10
W2 = W1 + 2
t = Table(data, repeatRows=0, colWidths=[W2, None]+[W1 for i in range(24*3)])
t = Table(data, repeatRows=0, colWidths=[W2, None]+[W1 for _ in range(word_length*3)])
t.setStyle(TableStyle(conf))
seed_samples = [[cell(w) for w in
@ -145,7 +150,7 @@ def doit(fname='worksheet.pdf'):
# page top-to-bottom
elements = []
elements.append(Paragraph('Seed XOR Worksheet',
elements.append(Paragraph(f'Seed XOR Worksheet{word_length} Words',
ParagraphStyle('tlab2', alignment=TA_LEFT, fontSize=16, spaceAfter=20, spaceBefore=20)))
elements.append(t)
@ -163,4 +168,5 @@ def doit(fname='worksheet.pdf'):
doc.build(elements)
doit()
doit("worksheet12.pdf", 12)
doit("worksheet.pdf", 24)

File diff suppressed because one or more lines are too long

86
worksheet12.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@ -37,7 +37,7 @@
---
# XOR Seed Example Using 3 Parts
# 24 Words XOR Seed Example Using 3 Parts
## Seed A (1 of 3)
@ -87,8 +87,56 @@
final word between: gas [300] - lend [3FF]
correct final word: indoor [398]
# 12 Words XOR Seed Example Using 3 Parts
## Seed A (1 of 3)
1=romance [5DC], 2=wink [7DE], 3=lottery [420], 4=autumn [07D], 5=shop [635], 6=bring [0E1],
7=dawn [1BF], 8=tongue [723], 9=range [58E], 10=crater [194], 11=truth [74E], 12=ability [001]
A = 5DC 7DE 420 07D 635 0E1 1BF 723 58E 194 74E 001
## Seed B (2 of 3)
1=boat [0C6], 2=unfair [768], 3=shell [62B], 4=violin [7A2], 5=tree [73F], 6=robust [5DA], 7=open [4D9],
8=ride [5CB], 9=visual [7A7], 10=forest [2D9], 11=vintage [7A1], 12=approve [056]
B = 0C6 768 62B 7A2 73F 5DA 4D9 5CB 7A7 2D9 7A1 056
## Seed C (3 of 3)
1=lion [411], 2=misery [46D], 3=divide [1FF], 4=hurry [37D], 5=latin [3EB], 6=fluid [2CD], 7=camp [106],
8=advance [01F], 9=illegal [388], 10=lab [3E0], 11=pyramid [578], 12=unhappy [76A]
C = 411 46D 1FF 37D 3EB 2CD 106 01F 388 3E0 578 76A
## Calculation (XOR each hex digit)
A = 5DC 7DE 420 07D 635 0E1 1BF 723 58E 194 74E 001
B = 0C6 768 62B 7A2 73F 5DA 4D9 5CB 7A7 2D9 7A1 056
C = 411 46D 1FF 37D 3EB 2CD 106 01F 388 3E0 578 76A
| | |
XOR = 10B 4DB 3F4 4A2 2E1 7F6 460 2F7 1A1 0AD 597 73x
## Resulting Seed Phrase
1=cannon [10B], 2=opinion [4DB], 3=leader [3F4], 4=nephew [4A2], 5=found [2E1], 6=yard [7F6],
7=metal [460], 8=galaxy [2F7], 9=crouch [1A1], 10=between [0AD], 11=real [597]
final word between: toward [730] - tree [73F]
correct final word: trade [735]
- 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, and there will be only one
- 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

104
xor.py
View File

@ -34,46 +34,54 @@ def xor_table(indent=' '*8):
- down to C, that is answer: a ⊕ b ⊕ c
''')
def get_words(h):
# Apply BIP39 to convert into seed words
v = int.from_bytes(h, 'big') << 8
w = []
for i in range(24):
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)
w.insert(0, m)
assert not v
indexes.insert(0, m)
# final csum_len bits are a checksum
indexes[-1] += sha256(entropy).digest()[0] >> (8 - csum_len)
return indexes
# final 8 bits are a checksum
w[-1] |= sha256(h).digest()[0]
return w
def calc_check(words):
assert len(words) == 24
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(24):
for i in range(words_len):
x <<= 11
x |= words[i]
x >>= 8
raw = x.to_bytes(32, 'big')
x >>= csum_len
raw = x.to_bytes(e_len, 'big')
cb = sha256(raw).digest()[0]
x <<= 8
cb = sha256(raw).digest()[0] >> (8 - csum_len)
x <<= csum_len
x |= cb
return raw, (x % 0x800)
def print_phrase(raw, indent=' '*4):
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 len(raw) == 32:
if from_entropy:
words = get_words(raw)
assert len(words) == 24
else:
words = list(raw)
assert len(words) in {23, 24}
x = ['%d=%s_[%03X]'%(n+1, wordlist[i],i) for n,i in enumerate(words)]
msg = ', '.join(x)
@ -83,18 +91,18 @@ def print_phrase(raw, indent=' '*4):
return eng, hx, words
def worked_example(count=3):
def worked_example(count=3, entropy_bytes=32):
rng = Random(123 * count)
print(f'# XOR Seed Example Using {count} Parts\n')
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 i in range(32))
raw = bytearray(rng.randint(0, 255) for _ in range(entropy_bytes))
eng, hx, words = print_phrase(raw, indent)
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))
@ -106,8 +114,14 @@ def worked_example(count=3):
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]) - 2):
for pos in range(len(digits[0]) - constant):
if digits[0][pos] == ' ':
xor += ' '
continue
@ -118,7 +132,7 @@ def worked_example(count=3):
assert 0 <= here < 16, here
xor += '%X' % here
xor += 'xx'
xor += 'x' * constant
lst = list(range(0, 100, 16)) + [23*4]
align = ''.join(('|' if n in lst else ' ') for n in range(len(xor)))
@ -132,20 +146,38 @@ def worked_example(count=3):
eng, hx, _ = print_phrase(rw, indent)
print('\n'.join(eng))
chk = int(xor.split(' ')[-1][0], 16) * 0x100
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[chk+0xff], chk+0xff))
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)
_, 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, and there will be only one
- 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
@ -159,6 +191,10 @@ if 1:
print('---\n')
if 1:
worked_example()
worked_example(3, entropy_bytes=32)
print("\n\n")
worked_example(3, entropy_bytes=16)
print("\n\n")
footer_text()