I'd an idea to compress BIP-39 seeds, make them to become a tiny string suitable for QR or RFID storage.
The process:
- We convert the wordlist into line numbers.
- We base58 encode the corresponding line number to the given word, so that each word will be turn into exactly 2 characters (zero padded)
- We can add a + sign to the end of the generated output or not, indicating if the seed is expanded with a unknown word or not
Eg:
Let's assume the following seed (freshly generated using BIP-39 tool):
cannon garment estate enforce remind attract about that east retreat uncle route method practice chunk
As QR this will generate a massive code that low definition cameras struggle to read. Representing a 102 characters long string.
Using the base58 word-to-line-number compressor, we get
5dEFBhBFS6345XuAdSQZeT2LMQP6d
, a 30 chars long string, rendering a much smaller and easy readable QR code.
Sample Python code for this (requires tkinter and the wordlist, named as english.txt in the same folder as the .py script - the wordlist can be found at BIP-39's Github [
https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt ]):
#!/usr/bin/python3
import os
import base64
import tkinter as tk
import tkinter.messagebox as messagebox
from typing import Mapping, Union
BITCOIN_ALPHABET = \
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
dir_path = os.path.dirname(__file__)
with open(f"{dir_path}/english.txt", "r") as f:
words = [line.strip() for line in f if line.strip()]
win = [Suspicious link removed]()
win.title("BIP-39 Compressor/Decompressor")
frame1 = tk.Frame(master=win)
frame1.grid(row=0, column=0, padx=5, pady=5)
label1 = tk.Label(master=frame1, text="Decompressed Seed")
label1.grid(row=0, column=0)
label2 = tk.Label(master=frame1, text="Your words BIP-39 seed")
label2.grid(row=0, column=2)
decomp = tk.Text(master=frame1, height=10)
decomp.grid(row=1, column=0, columnspan=3)
btn = tk.Button(master=frame1, text="Compress", command=lambda: compressSeed())
btn.grid(row=2, column=2)
frame2 = tk.Frame(master=win)
frame2.grid(row=1, column=0, padx=5, pady=5)
label3 = tk.Label(master=frame2, text="Compressed Seed")
label3.grid(row=0, column=0)
label4 = tk.Label(master=frame2, text="Your compressed seed")
label4.grid(row=0, column=2)
comp = tk.Text(master=frame2, height=1)
comp.grid(row=1, column=0, columnspan=3)
btn2 = tk.Button(master=frame2, text="Decompress", command=lambda: decompressSeed())
btn2.grid(row=2, column=2)
expanded_var = tk.BooleanVar()
check_btn = tk.Checkbutton(
master=win,
text="Mark as Expanded (adds '+' to output)",
variable=expanded_var,
)
check_btn.grid(row=3, column=0, sticky='w', pady=10)
def b58encode(i: int, default_one: bool = True, alphabet: bytes = BITCOIN_ALPHABET) -> str:
if not i and default_one:
return alphabet[0:1]
string = ""
base = len(alphabet)
while i:
i, idx = divmod(i, base)
string = alphabet[idx:idx+1] + string
return string
def b58decode(v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET,
*, autofix: bool = False) -> int:
map = _get_base58_decode_map(alphabet, autofix=autofix)
decimal = 0
base = len(alphabet)
for char in v:
decimal = decimal * base + map[char]
return decimal
def _get_base58_decode_map(alphabet: bytes,
autofix: bool) -> Mapping[int, int]:
invmap = {char: index for index, char in enumerate(alphabet)}
groups = [b'0Oo', b'Il1']
for group in groups:
pivots = [c for c in group if c in invmap]
if len(pivots) == 1:
for alternative in group:
invmap[alternative] = invmap[pivots[0]]
return invmap
def compressSeed():
bip_words = decomp.get("1.0", "end-1c").strip().split()
out = []
for word in bip_words:
if word in words:
pos = words.index(word)
out.append(b58encode(pos + 1))
else:
messagebox.showinfo("Seed Error", "The word " + word + " wasn't found in the dictionary!")
compressed_seed = "".join(out)
# Append '+' if expanded option is selected
if expanded_var.get():
compressed_seed += '+'
comp.delete("1.0", "end-1c")
comp.insert("1.0", compressed_seed)
def decompressSeed():
compressed_seed = comp.get("1.0", "end-1c").strip()
# Remove trailing '+' if present (expanded seed indicator)
if compressed_seed.endswith('+'):
compressed_seed = compressed_seed[:-1]
messagebox.showinfo("Expanded Seed Alert", "The seed appears to be expanded with an unknown secret extra word!")
decompressed_words = []
i = 0
while i < len(compressed_seed):
chunk = compressed_seed[i:i+2]
if not chunk:
break
pos = b58decode(chunk)
decompressed_words.append(words[pos - 1])
i += 2
decomp.delete("1.0", "end-1c")
decomp.insert("1.0", " ".join(decompressed_words))
win.mainloop()
Footnote: sorry for posting from a new account, but I don't know where's my TOTP code (and don't quite remember to had create one) for my old account.