Seedvault refactor, more on KT
This commit is contained in:
parent
44dc30a57a
commit
5c463e5cde
@ -13,7 +13,7 @@ NFC, passive websites, and QR/BBQr codes.
|
||||
|
||||
- Receiver picks an EC keypair, stores it in settings, and publishes the pubkey via a QR/NFC
|
||||
- Sender gets that, pickes own keypair, and does ECDH to arrive at a shared session key
|
||||
- Sender picks a human-readable secret (12 words) which is independant of anything else (P key)
|
||||
- Sender picks a human-readable secret which is independant of anything else (P key)
|
||||
- The secret data (perhaps a seed phrase, XPRV, secure note, etc) is AES encryped with P key,
|
||||
then encrypted + MAC added with session key
|
||||
- Data packet is sent to receiver, who can reconstruct the session key via ECDH
|
||||
@ -40,9 +40,7 @@ NFC, passive websites, and QR/BBQr codes.
|
||||
|
||||
The first byte encodes what the package contents (under all the encryption).
|
||||
|
||||
- `w` - 12/18/24 words - 16/24/32 bytes follow
|
||||
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
|
||||
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
|
||||
- `s` - 12/18/24 words/raw master/xprv - 16/24/32/64 bytes follow encoded in internal format
|
||||
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
|
||||
- `n` - one or many notes export (JSON array)
|
||||
- `v` - seed vault export (JSON: one secret key but includes includes name, source of key)
|
||||
@ -58,10 +56,9 @@ Base32 encoded.
|
||||
|
||||
New type codes for BBQr are defined for the purposes of this application:
|
||||
|
||||
- `R` contains `(pubkey)` ... begins the process from receiver; pubkey is base32 of 33 bytes
|
||||
- `S` contains `(pubkey)(data)` ... data from sender; all base32 encoded, and first 33 bytes
|
||||
are sender's pubkey
|
||||
- `E` for PSBT: `(randint)(data)` ... randint (4 bytes) indicates which a randomly
|
||||
- `R` contains `(pubkey)` ... begins the process from receiver; compressed pubkey is 33 bytes
|
||||
- `S` contains `(pubkey)(data)` ... data from sender; first 33 bytes are sender's pubkey
|
||||
- `E` for PSBT: `(randint)(data)` ... randint (4 bytes) indicates which randomly
|
||||
selected derived subkey from pre-shared xpub associated with receiver
|
||||
|
||||
All the data is encrypted with the exception of the pubkey or randint. Keep in mind
|
||||
@ -81,8 +78,8 @@ receive the package. The sender's keypair is implied by:
|
||||
|
||||
.../20250318/(randint)
|
||||
|
||||
Because both the sender and receiver have each other's XPUB they can derive
|
||||
the appropriate pubkeys (and privkey for their side) without communicating anymore
|
||||
Because both the sender and receiver already have each other's XPUB they can derive
|
||||
the appropriate pubkeys (and privkey for their side) without communicating
|
||||
more than `randint`. The sending COLDCARD will pick a new random value each time.
|
||||
|
||||
## Encryption Details
|
||||
@ -91,16 +88,22 @@ AES-256-CTR is used exclusively. Session key is picked via ECDH with final
|
||||
key value being the SHA256 over 64 bytes of coordinate X (concat) Y.
|
||||
|
||||
While ECDH is enough to assure privacy from men in the middle, we
|
||||
add an additional layer of encryption, using 12 BIP-39 words with
|
||||
checksum (128 bits). We call this the "paranoid key" internally.
|
||||
add an additional layer of encryption. We call this the "paranoid key" internally
|
||||
and in the UX it is called "Teleport Password".
|
||||
|
||||
The user sees a random 8-character password, generated as a random 40-bit value, but
|
||||
shown in Base32 (8 chars) for the human to enter. We apply PBKDF2-SHA512 with
|
||||
an interation count of 5000 to stretch that to 512 bits, of which we use half.
|
||||
The session key is used as the key for the KDF, and the entered value as salt.
|
||||
|
||||
- ECDH arrives at session key
|
||||
- decrypt (AES-256-CTR) the binary body of message
|
||||
- verify checksum:
|
||||
- final 2 bytes should be `== SHA256(decrypted body[0:-2])[-2:]`
|
||||
- if not, corruption, truncation, or wrong keys
|
||||
- if that decryption is correct, then prompt user for the paranoid key (12 words)
|
||||
- concat paranoid key (16 bytes) with first 16 bytes of the session key and run AES-256-CTR again
|
||||
- if that decryption is correct, then prompt user for the paranoid key (8 chars)
|
||||
- stretch that value using session key and 5000 iterations of PBKDF2-SHA512
|
||||
- use upper 256 bits and run AES-256-CTR again
|
||||
- same checksum of 2 bytes of SHA256 are found inside after decryption
|
||||
|
||||
Encryption adds 4 bytes of overhead because of these MAC values,
|
||||
@ -114,23 +117,32 @@ publish a static website directly from an open Github repository.
|
||||
The single-page website contains javascript code which looks at the
|
||||
"hash" part of the incoming URL (`window.location.hash`) and if it
|
||||
meets the requirements, renders a large QR. The QR data must look like
|
||||
a correctly-ecoded BBQr with one of the type codes above.
|
||||
(otherwise the website could render any QR, which we don't want to
|
||||
support).
|
||||
a correctly-encoded BBQr with one of the 3 type-codes above (`R` `S` or `E`).
|
||||
Otherwise the website could render any QR, which we don't want to
|
||||
support.
|
||||
|
||||
The page will offer "copy to clipboard" features for the data inside
|
||||
the QR as a URL (ie. same URL as shown) and as an image and of course,
|
||||
the COLDCARD Q can scan from the web browser screen itself.
|
||||
|
||||
On the COLDCARD side, when NFC is tapped, it will offer a long
|
||||
URL to this site with the data to be transfered "after the hash".
|
||||
This is optional since the QR shown on the Q itself, would pass
|
||||
the same data.
|
||||
When the BBQr data is larger than comfortable for a single QR, the
|
||||
website can split into a multi-frame BBQr. The website can
|
||||
do this without understanding the contents of the BBQr data (all
|
||||
of which is encrypted). Download options will be provided for
|
||||
single-frame QR, animated PNG, and "stacked BBQr" (a single tall
|
||||
PNG with each QR frame stacked).
|
||||
|
||||
On the COLDCARD side, when NFC is tapped, it will offer a long URL
|
||||
to this site with the data to be transfered "after the hash". This
|
||||
is optional since the QR can be shown on the Q itself, and would
|
||||
pass the same data.
|
||||
|
||||
Since the website is running on Github, Coinkite does not have
|
||||
access to IP addresses or other log details. Because the data for
|
||||
teleport is "after the hash" it is never sent to Github's servers
|
||||
but remains in the browser only.
|
||||
but remains in the browser only. All JS resources referenced by the
|
||||
webpage will have content hashes applied to prevent interference,
|
||||
and the site will be served over SSL.
|
||||
|
||||
# UX Details
|
||||
|
||||
@ -146,6 +158,4 @@ but remains in the browser only.
|
||||
signed) and the QR is prepared for that receiver. They should get another
|
||||
chance to do the same for the other possible co-signers.
|
||||
|
||||
- If the user opts to skip the "paranoid key" then treat it as `bytes([0x5a] * 32)`,
|
||||
but still do the extra decryption and MAC check.
|
||||
|
||||
|
||||
@ -1292,11 +1292,11 @@ async def verify_backup(*A):
|
||||
# do a limited CRC-check over encrypted file
|
||||
await backups.verify_backup_file(fn)
|
||||
|
||||
async def import_extended_key_as_secret(extended_key, ephemeral, meta=None):
|
||||
async def import_extended_key_as_secret(extended_key, ephemeral, origin=None):
|
||||
try:
|
||||
import seed
|
||||
if ephemeral:
|
||||
await seed.set_ephemeral_seed_extended_key(extended_key, meta=meta)
|
||||
await seed.set_ephemeral_seed_extended_key(extended_key, origin=origin)
|
||||
else:
|
||||
await seed.set_seed_extended_key(extended_key)
|
||||
except ValueError:
|
||||
@ -1360,7 +1360,7 @@ async def import_xprv(_1, _2, item):
|
||||
extended_key = ln
|
||||
break
|
||||
|
||||
await import_extended_key_as_secret(extended_key, ephemeral, meta='Imported XPRV')
|
||||
await import_extended_key_as_secret(extended_key, ephemeral, origin='Imported XPRV')
|
||||
# not reached; will do reset.
|
||||
|
||||
async def need_clear_seed(*a):
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import compat7z, stash, ckcc, chains, gc, sys, bip39, uos, ngu
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from utils import pad_raw_secret
|
||||
from utils import deserialize_secret
|
||||
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_input_text
|
||||
import version, ujson
|
||||
from uio import StringIO
|
||||
@ -127,7 +127,7 @@ def extract_raw_secret(chain, vals):
|
||||
assert 'raw_secret' in vals
|
||||
rs = vals.pop('raw_secret')
|
||||
|
||||
raw = pad_raw_secret(rs)
|
||||
raw = deserialize_secret(rs)
|
||||
|
||||
# check we can decode this right (might be different firmare)
|
||||
opmode, bits, node = stash.SecretStash.decode(raw)
|
||||
@ -277,7 +277,7 @@ async def restore_tmp_from_dict_ll(vals):
|
||||
from seed import set_ephemeral_seed
|
||||
from actions import goto_top_menu
|
||||
|
||||
await set_ephemeral_seed(raw, chain, meta="Coldcard Backup")
|
||||
await set_ephemeral_seed(raw, chain, origin="Coldcard Backup")
|
||||
for k, v in vals.items():
|
||||
if not k[:8] == "setting.":
|
||||
continue
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#
|
||||
import gc, chains, version, ngu, web2fa, bip39, re
|
||||
from chains import NLOCK_IS_TIME
|
||||
from utils import swab32, xfp2str, truncate_address, pad_raw_secret, show_single_address
|
||||
from utils import swab32, xfp2str, truncate_address, deserialize_secret, show_single_address
|
||||
from glob import settings, dis
|
||||
from ux import ux_confirm, ux_show_story, the_ux, OK, ux_dramatic_pause, ux_enter_number, ux_aborted
|
||||
from menu import MenuSystem, MenuItem, start_chooser
|
||||
@ -45,7 +45,7 @@ class CCCFeature:
|
||||
def get_encoded_secret(cls):
|
||||
# Gets the key C as encoded binary secret, compatible w/
|
||||
# encodings used in stash.
|
||||
return pad_raw_secret(settings.get('ccc')['secret'])
|
||||
return deserialize_secret(settings.get('ccc')['secret'])
|
||||
|
||||
@classmethod
|
||||
def get_xfp(cls):
|
||||
@ -369,7 +369,7 @@ be ready to show it as a QR, before proceeding.'''
|
||||
from actions import goto_top_menu
|
||||
|
||||
enc = CCCFeature.get_encoded_secret()
|
||||
await set_ephemeral_seed(enc, meta='Key C from CCC')
|
||||
await set_ephemeral_seed(enc, origin='Key C from CCC')
|
||||
|
||||
goto_top_menu()
|
||||
|
||||
@ -725,10 +725,10 @@ async def gen_or_import():
|
||||
|
||||
elif ch == '6':
|
||||
# pick existing from Seed Vault
|
||||
enc = await SeedVaultChooserMenu.pick(words_only=True)
|
||||
if not enc: return None
|
||||
words = SecretStash.decode_words(enc)
|
||||
await enable_step1(words)
|
||||
picked = await SeedVaultChooserMenu.pick(words_only=True)
|
||||
if picked:
|
||||
words = SecretStash.decode_words(deserialize_secret(picked.encoded))
|
||||
await enable_step1(words)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@ -263,7 +263,7 @@ async def drv_entro_step2(_1, picked, _2, just_pick=False):
|
||||
xfp_str = xfp2str(settings.get("xfp", 0))
|
||||
await seed.set_ephemeral_seed(
|
||||
encoded,
|
||||
meta='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
origin='BIP85 Derived from [%s], index=%d' % (xfp_str, index)
|
||||
)
|
||||
goto_top_menu()
|
||||
break
|
||||
|
||||
@ -665,7 +665,7 @@ class NFCHandler:
|
||||
if winner:
|
||||
try:
|
||||
from seed import set_ephemeral_seed_words
|
||||
await set_ephemeral_seed_words(winner, meta='NFC Import')
|
||||
await set_ephemeral_seed_words(winner, origin='NFC Import')
|
||||
except Exception as e:
|
||||
#import sys; sys.print_exception(e)
|
||||
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
|
||||
|
||||
@ -635,7 +635,8 @@ async def import_from_json(records):
|
||||
|
||||
was = list(settings.get('notes', []))
|
||||
was.extend(new)
|
||||
settings.put('notes', was)
|
||||
settings.set('notes', was)
|
||||
settings.set('secnap', True)
|
||||
settings.save()
|
||||
|
||||
except Exception as e:
|
||||
|
||||
127
shared/seed.py
127
shared/seed.py
@ -13,7 +13,8 @@
|
||||
import ngu, uctypes, bip39, random, version
|
||||
from ucollections import OrderedDict
|
||||
from menu import MenuItem, MenuSystem
|
||||
from utils import xfp2str, parse_extended_key, swab32, pad_raw_secret, problem_file_line, wipe_if_deltamode
|
||||
from utils import xfp2str, parse_extended_key, swab32
|
||||
from utils import deserialize_secret, problem_file_line, wipe_if_deltamode
|
||||
from uhashlib import sha256
|
||||
from ux import ux_show_story, the_ux, ux_dramatic_pause, ux_confirm, OK, X
|
||||
from ux import PressRelease, ux_input_text, show_qr_code
|
||||
@ -27,13 +28,22 @@ from nvstore import SettingsObject
|
||||
from files import CardMissingError, needs_microsd, CardSlot
|
||||
from charcodes import KEY_QR, KEY_ENTER, KEY_CANCEL, KEY_CLEAR
|
||||
from uasyncio import sleep_ms
|
||||
|
||||
from ucollections import namedtuple
|
||||
|
||||
# seed words lengths we support: 24=>256 bits, and recommended
|
||||
VALID_LENGTHS = (24, 18, 12)
|
||||
|
||||
# bit flag that means "also include bare prefix as a valid word"
|
||||
_PREFIX_MARKER = const(1<<26)
|
||||
|
||||
# what we store (in JSON as a tuple) for each seed vault key.
|
||||
# - 'encoded' is hex, and has is trimmed of right side zeros
|
||||
VaultEntry = namedtuple('VaultEntry', 'xfp encoded label origin')
|
||||
|
||||
def seed_vault_iter():
|
||||
# iterate over all seeds in the vault; returns VaultEntry instances.
|
||||
for tup in settings.master_get("seeds", []):
|
||||
yield VaultEntry(*tup)
|
||||
|
||||
def letter_choices(sofar='', depth=0, thres=5):
|
||||
# make a list of word completions based on indicated prefix
|
||||
@ -413,14 +423,16 @@ async def new_from_dice(nwords):
|
||||
|
||||
def in_seed_vault(encoded):
|
||||
# Test if indicated secret is in the seed vault already.
|
||||
seeds = settings.master_get("seeds", [])
|
||||
if seeds:
|
||||
ss = SecretStash.storage_serialize(encoded)
|
||||
if ss in [s[1] for s in seeds]:
|
||||
hss = None
|
||||
for rec in seed_vault_iter():
|
||||
if not hss:
|
||||
hss = SecretStash.storage_serialize(encoded)
|
||||
if hss == rec.encoded:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def add_seed_to_vault(encoded, meta=None):
|
||||
async def add_seed_to_vault(encoded, origin=None, label=None):
|
||||
|
||||
if not settings.master_get("seedvault", False):
|
||||
# seed vault disabled
|
||||
@ -460,10 +472,9 @@ async def add_seed_to_vault(encoded, meta=None):
|
||||
return
|
||||
|
||||
# Save it into master settings
|
||||
seeds.append((new_xfp_str,
|
||||
SecretStash.storage_serialize(encoded),
|
||||
xfp_ui,
|
||||
meta))
|
||||
rec = VaultEntry(xfp=new_xfp_str, encoded=SecretStash.storage_serialize(encoded),
|
||||
label=(label or xfp_ui), origin=origin)
|
||||
seeds.append(tuple(rec))
|
||||
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
@ -472,10 +483,10 @@ async def add_seed_to_vault(encoded, meta=None):
|
||||
return True
|
||||
|
||||
async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||
is_restore=False, meta=None):
|
||||
is_restore=False, origin=None, label=None):
|
||||
# Capture tmp seed into vault, if so enabled, and regardless apply it as new tmp.
|
||||
if not is_restore:
|
||||
await add_seed_to_vault(encoded, meta=meta)
|
||||
await add_seed_to_vault(encoded, origin=origin, label=label)
|
||||
dis.fullscreen("Wait...")
|
||||
|
||||
applied, err_msg = pa.tmp_secret(encoded, chain=chain, bip39pw=bip39pw)
|
||||
@ -492,11 +503,11 @@ async def set_ephemeral_seed(encoded, chain=None, summarize_ux=True, bip39pw='',
|
||||
|
||||
return applied
|
||||
|
||||
async def set_ephemeral_seed_words(words, meta):
|
||||
async def set_ephemeral_seed_words(words, origin):
|
||||
dis.progress_bar_show(0.1)
|
||||
encoded = seed_words_to_encoded_secret(words)
|
||||
dis.progress_bar_show(0.5)
|
||||
await set_ephemeral_seed(encoded, meta=meta)
|
||||
await set_ephemeral_seed(encoded, origin=origin)
|
||||
goto_top_menu()
|
||||
|
||||
async def ephemeral_seed_generate_from_dice(nwords):
|
||||
@ -513,7 +524,7 @@ async def ephemeral_seed_generate_from_dice(nwords):
|
||||
words = await approve_word_list(seed, nwords, ephemeral=True)
|
||||
if words:
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta='Dice')
|
||||
await set_ephemeral_seed_words(words, origin='Dice')
|
||||
|
||||
def generate_seed():
|
||||
# Generate 32 bytes of best-quality high entropy TRNG bytes.
|
||||
@ -536,7 +547,7 @@ async def make_new_wallet(nwords):
|
||||
async def ephemeral_seed_import(nwords):
|
||||
async def import_done_cb(words):
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta='Imported')
|
||||
await set_ephemeral_seed_words(words, origin='Imported')
|
||||
|
||||
if version.has_qwerty:
|
||||
from ux_q1 import seed_word_entry
|
||||
@ -550,17 +561,17 @@ async def ephemeral_seed_generate(nwords):
|
||||
words = await approve_word_list(seed, nwords, ephemeral=True)
|
||||
if words:
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed_words(words, meta="TRNG Words")
|
||||
await set_ephemeral_seed_words(words, origin="TRNG Words")
|
||||
|
||||
async def set_seed_extended_key(extended_key):
|
||||
encoded, chain = xprv_to_encoded_secret(extended_key)
|
||||
set_seed_value(encoded=encoded, chain=chain)
|
||||
goto_top_menu(first_time=True)
|
||||
|
||||
async def set_ephemeral_seed_extended_key(extended_key, meta=None):
|
||||
async def set_ephemeral_seed_extended_key(extended_key, origin=None):
|
||||
encoded, chain = xprv_to_encoded_secret(extended_key)
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed(encoded=encoded, chain=chain, meta=meta)
|
||||
await set_ephemeral_seed(encoded=encoded, chain=chain, origin=origin)
|
||||
goto_top_menu()
|
||||
|
||||
async def approve_word_list(seed, nwords, ephemeral=False):
|
||||
@ -680,7 +691,7 @@ async def calc_bip39_passphrase(pw, bypass_tmp=False):
|
||||
async def set_bip39_passphrase(pw, bypass_tmp=False, summarize_ux=True):
|
||||
nv, xfp, parent_xfp = await calc_bip39_passphrase(pw, bypass_tmp=bypass_tmp)
|
||||
ret = await set_ephemeral_seed(nv, summarize_ux=summarize_ux, bip39pw=pw,
|
||||
meta="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||
origin="BIP-39 Passphrase on [%s]" % xfp2str(parent_xfp))
|
||||
dis.draw_status(bip39=int(bool(pw)), xfp=xfp, tmp=1)
|
||||
return ret
|
||||
|
||||
@ -821,7 +832,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
dis.fullscreen("Applying...")
|
||||
|
||||
xfp, encoded = item.arg
|
||||
encoded = item.arg # 72 bytes binary
|
||||
|
||||
await set_ephemeral_seed(encoded, is_restore=True)
|
||||
|
||||
@ -833,15 +844,15 @@ class SeedVaultMenu(MenuSystem):
|
||||
|
||||
esc = ""
|
||||
tmp_val = False
|
||||
idx, xfp_str, encoded = item.arg
|
||||
idx, rec, encoded = item.arg
|
||||
current_active = (pa.tmp_value == bytes(encoded))
|
||||
|
||||
msg = "Remove seed from seed vault "
|
||||
msg = "Remove seed from seed vault"
|
||||
if pa.tmp_value and current_active:
|
||||
tmp_val = True
|
||||
msg += "?\n\n"
|
||||
else:
|
||||
msg += ("and delete its settings?\n\n"
|
||||
msg += (" and delete its settings?\n\n"
|
||||
"Press %s to continue, press (1) to "
|
||||
"only remove from seed vault and keep "
|
||||
"encrypted settings for later use.\n\n") % OK
|
||||
@ -849,7 +860,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
|
||||
msg += "WARNING: Funds will be lost if wallet is not backed-up elsewhere."
|
||||
|
||||
ch = await ux_show_story(title="[" + xfp_str + "]", msg=msg, escape=esc)
|
||||
ch = await ux_show_story(title="[" + rec.xfp + "]", msg=msg, escape=esc)
|
||||
if ch == "x": return
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
@ -883,13 +894,13 @@ class SeedVaultMenu(MenuSystem):
|
||||
|
||||
@staticmethod
|
||||
async def _detail(menu, label, item):
|
||||
xfp_str, encoded, name, meta = item.arg
|
||||
rec, encoded = item.arg
|
||||
|
||||
# - first byte represents type of secret (internal encoding flag)
|
||||
# - first byte represents type of secret (internal encoding flags)
|
||||
txt = SecretStash.summary(encoded[0])
|
||||
|
||||
detail = "Name:\n%s\n\nMaster XFP:\n%s\n\nOrigin:\n%s\n\nSecret Type:\n%s" \
|
||||
% (name, xfp_str, meta, txt)
|
||||
detail = "Name:\n%s\n\nMaster XFP: %s\nSecret Type: %s\n\nOrigin:\n%s\n\n" \
|
||||
% (rec.label, rec.xfp, txt, rec.origin)
|
||||
|
||||
await ux_show_story(detail)
|
||||
|
||||
@ -899,28 +910,28 @@ class SeedVaultMenu(MenuSystem):
|
||||
from glob import dis
|
||||
from ux import ux_input_text
|
||||
|
||||
idx, xfp_str = item.arg
|
||||
idx, old = item.arg
|
||||
|
||||
seeds = settings.master_get("seeds", [])
|
||||
chk_xfp, encoded, old_name, meta = seeds[idx]
|
||||
assert chk_xfp == xfp_str
|
||||
#seeds = settings.master_get("seeds", [])
|
||||
#old = VaultEntry(*seeds[idx])
|
||||
assert old.xfp == xfp_str
|
||||
|
||||
new_name = await ux_input_text(old_name, confirm_exit=False, max_len=40)
|
||||
new_label = await ux_input_text(old.label, confirm_exit=False, max_len=40)
|
||||
|
||||
if not new_name:
|
||||
if not new_label:
|
||||
return
|
||||
|
||||
dis.fullscreen("Saving...")
|
||||
|
||||
# save it
|
||||
seeds[idx] = (chk_xfp, encoded, new_name, meta)
|
||||
seeds[idx] = VaultEntry(old.xfp, old.encoded, new_label, old.origin)
|
||||
|
||||
# need to load and work on master secrets, will be slow if on tmp seed
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
# update label in sub-menu
|
||||
menu.items[0].label = new_name
|
||||
menu.items[0].arg = menu.items[0].arg[0:2] + (new_name,) + menu.items[0].arg[3:]
|
||||
menu.items[0].label = new_label
|
||||
menu.items[0].arg = seeds[idx]
|
||||
|
||||
# .. and name in parent menu too
|
||||
parent = the_ux.parent_of(menu)
|
||||
@ -950,10 +961,9 @@ class SeedVaultMenu(MenuSystem):
|
||||
seeds = settings.master_get("seeds", [])
|
||||
|
||||
# Save it into master settings
|
||||
seeds.append((new_xfp_str,
|
||||
seeds.append(VaultEntry(new_xfp_str,
|
||||
SecretStash.storage_serialize(pa.tmp_value),
|
||||
xfp_ui,
|
||||
"unknown origin"))
|
||||
xfp_ui, "unknown origin"))
|
||||
|
||||
settings.master_set("seeds", seeds)
|
||||
|
||||
@ -970,7 +980,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
rv = []
|
||||
add_current_tmp = MenuItem("Add current tmp", f=cls._add_current_tmp)
|
||||
|
||||
seeds = settings.master_get("seeds", [])
|
||||
seeds = list(seed_vault_iter())
|
||||
|
||||
if not seeds:
|
||||
rv.append(MenuItem('(none saved yet)'))
|
||||
@ -981,16 +991,19 @@ class SeedVaultMenu(MenuSystem):
|
||||
wipe_if_deltamode()
|
||||
|
||||
tmp_in_sv = False
|
||||
for i, (xfp_str, encoded, name, meta) in enumerate(seeds):
|
||||
for i, rec in enumerate(seeds):
|
||||
is_active = False
|
||||
encoded = pad_raw_secret(encoded)
|
||||
|
||||
# de-serialize encoded secret
|
||||
encoded = deserialize_secret(rec.encoded)
|
||||
if encoded == pa.tmp_value:
|
||||
is_active = tmp_in_sv = True
|
||||
|
||||
submenu = [
|
||||
MenuItem(name, f=cls._detail, arg=(xfp_str, encoded, name, meta)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=(xfp_str, encoded)),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, xfp_str)),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, xfp_str, encoded)),
|
||||
MenuItem(rec.label, f=cls._detail, arg=(rec, encoded)),
|
||||
MenuItem('Use This Seed', f=cls._set, arg=encoded),
|
||||
MenuItem('Rename', f=cls._rename, arg=(i, rec)),
|
||||
MenuItem('Delete', f=cls._remove, arg=(i, rec, encoded)),
|
||||
]
|
||||
if is_active:
|
||||
submenu[1] = MenuItem("Seed In Use")
|
||||
@ -1001,7 +1014,7 @@ class SeedVaultMenu(MenuSystem):
|
||||
# DO NOT offer any modification api (rename/delete)
|
||||
submenu = submenu[:2]
|
||||
|
||||
item = MenuItem('%2d: %s' % (i+1, name), menu=MenuSystem(submenu))
|
||||
item = MenuItem('%2d: %s' % (i+1, rec.label), menu=MenuSystem(submenu))
|
||||
if is_active:
|
||||
item.is_chosen = lambda: True
|
||||
|
||||
@ -1027,18 +1040,14 @@ class SeedVaultChooserMenu(MenuSystem):
|
||||
def __init__(self, words_only=False):
|
||||
self.result = None
|
||||
|
||||
seeds = settings.master_get("seeds", [])
|
||||
items = []
|
||||
|
||||
for i, (xfp_str, encoded, name, meta) in enumerate(seeds):
|
||||
encoded = pad_raw_secret(encoded)
|
||||
if words_only and not SecretStash.is_words(encoded):
|
||||
for i, rec in enumerate(seed_vault_iter()):
|
||||
if words_only and not SecretStash.is_words(deserialize_secret(rec.encoded)):
|
||||
continue
|
||||
|
||||
item = MenuItem('%2d: %s' % (i+1, name), arg=encoded, f=self.picked)
|
||||
item = MenuItem('%2d: %s' % (i+1, rec.label), arg=rec, f=self.picked)
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
else:
|
||||
items.append(MenuItem("(none suitable)"))
|
||||
|
||||
super().__init__(items)
|
||||
@ -1324,7 +1333,7 @@ async def apply_pass_value(new_pp):
|
||||
return
|
||||
|
||||
await set_ephemeral_seed(nv, summarize_ux=False, bip39pw=new_pp,
|
||||
meta="BIP-39 Passphrase on [%s]" % parent_xfp_str)
|
||||
origin="BIP-39 Passphrase on [%s]" % parent_xfp_str)
|
||||
|
||||
if ch == '1':
|
||||
try:
|
||||
|
||||
@ -167,6 +167,7 @@ class SecretStash:
|
||||
@staticmethod
|
||||
def storage_serialize(secret):
|
||||
# make it a JSON-compatible field
|
||||
# - converse: utils.deserialize_secret()
|
||||
return B2A(bytes(secret).rstrip(b"\x00"))
|
||||
|
||||
@staticmethod
|
||||
@ -422,13 +423,4 @@ class SensitiveValues:
|
||||
self.register(pk)
|
||||
return pk
|
||||
|
||||
def encoded_secret(self):
|
||||
# we do not support master as secret - only extended keys and mnemonics
|
||||
if self.mode == "xprv":
|
||||
nv = SecretStash.encode(xprv=self.node)
|
||||
else:
|
||||
assert self.mode == "words"
|
||||
nv = SecretStash.encode(seed_phrase=self.raw)
|
||||
return nv
|
||||
|
||||
# EOF
|
||||
|
||||
@ -33,7 +33,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
from pincodes import pa
|
||||
assert pa.is_secret_blank() # "must not have secret"
|
||||
|
||||
meta = "from "
|
||||
origin = "from "
|
||||
label = "TAPSIGNER encrypted backup file"
|
||||
choice = await import_export_prompt(label, is_import=True)
|
||||
|
||||
@ -69,7 +69,7 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
else:
|
||||
fn = await file_picker(suffix="aes", min_size=100, max_size=160, **choice)
|
||||
if not fn: return
|
||||
meta += (" (%s)" % fn)
|
||||
origin += (" (%s)" % fn)
|
||||
try:
|
||||
with CardSlot(**choice) as card:
|
||||
with open(fn, 'rb') as fp:
|
||||
@ -103,6 +103,6 @@ async def import_tapsigner_backup_file(_1, _2, item):
|
||||
await ux_show_story(title="FAILURE", msg=str(e))
|
||||
continue
|
||||
|
||||
await import_extended_key_as_secret(extended_key, ephemeral, meta=meta)
|
||||
await import_extended_key_as_secret(extended_key, ephemeral, origin=origin)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -3,15 +3,16 @@
|
||||
# teleport.py - Magically transport extremely sensitive data between the
|
||||
# secure environment of two Q's.
|
||||
#
|
||||
import utime, uzlib, ngu, aes256ctr, bip39
|
||||
from utils import problem_file_line, xor
|
||||
import sys, uzlib, ngu, aes256ctr, bip39, json, stash
|
||||
from utils import problem_file_line, B2A, xfp2str, deserialize_secret
|
||||
from ubinascii import unhexlify as a2b_hex
|
||||
from ubinascii import hexlify as b2a_hex
|
||||
from glob import settings
|
||||
from ux import ux_show_story, ux_confirm, show_qr_code
|
||||
from ux_q1 import show_bbqr_codes, QRScannerInteraction, seed_word_entry, ux_render_words
|
||||
from ux import ux_show_story, ux_confirm, show_qr_code, the_ux, ux_dramatic_pause
|
||||
from ux_q1 import show_bbqr_codes, QRScannerInteraction, ux_input_text
|
||||
from charcodes import KEY_QR, KEY_NFC, KEY_CANCEL
|
||||
from bbqr import int2base36, b32encode
|
||||
from bbqr import b32encode, b32decode
|
||||
from menu import MenuItem, MenuSystem
|
||||
|
||||
KT_DOMAIN = 'keyteleport.com'
|
||||
|
||||
@ -30,8 +31,8 @@ KT_DOMAIN = 'keyteleport.com'
|
||||
|
||||
def short_bbqr(type_code, data):
|
||||
# Short-circuit basic BBQr encoding here: always Base32, single part: 1 of 1
|
||||
#hdr = 'B$' + encoding + type_code + int2base36(num_parts) + int2base36(pkt)
|
||||
hdr = 'B$2' + type_code + int2base36(1) + int2base36(0)
|
||||
# XXX generalize
|
||||
hdr = 'B$2%s0100' % type_code
|
||||
|
||||
return hdr + b32encode(data)
|
||||
|
||||
@ -79,14 +80,15 @@ We will re-use same values as last try, unless you press (R) for new values to b
|
||||
settings.set("ktrx", b2a_hex(kp.privkey()))
|
||||
settings.save()
|
||||
|
||||
pubkey = kp.pubkey().to_bytes(False) # compressed format
|
||||
pubkey = kp.pubkey().to_bytes() # default: compressed format
|
||||
assert pubkey[0] in { 2, 3}
|
||||
|
||||
msg = '''You are starting a teleport of sensitive data from another COLDCARD.\n
|
||||
msg = '''You are starting a teleport of sensitive data from another COLDCARD. \
|
||||
It will be double-encrypted with AES-256-CTR using ECDH for one-time key and also \
|
||||
an optional passphrase.\n
|
||||
You must show this QR code to the sender somehow. %s to show now''' % KEY_QR
|
||||
a password.\n
|
||||
Show the QR on next screen to the sender somehow. ENTER or %s to show here''' % KEY_QR
|
||||
|
||||
await tk_show_payload('R', pubkey, 'Key Teleport: Receive', 'Show to Sender', msg)
|
||||
await tk_show_payload('R', pubkey, 'Key Teleport: Receive', cta='Show to Sender', msg=msg)
|
||||
|
||||
async def tk_show_payload(type_code, pubkey, title, cta=None, msg=None):
|
||||
# show the QR and/or NFC
|
||||
@ -116,41 +118,69 @@ async def tk_show_payload(type_code, pubkey, title, cta=None, msg=None):
|
||||
await nfc_push_kt(qr)
|
||||
elif ch == KEY_QR or ch == 'y':
|
||||
await show_qr_code(qr, is_alnum=True, msg=cta, force_msg=True, allow_nfc=False)
|
||||
|
||||
if not msg: break
|
||||
elif ch == 'x':
|
||||
return
|
||||
|
||||
def valid_looking_pubkey(rx_pubkey):
|
||||
try:
|
||||
assert rx_pubkey[0] in { 2, 3}
|
||||
assert len(rx_pubkey) == 33
|
||||
assert len(set(rx_pubkey)) > 3
|
||||
# check on curve? secp256k1.ecdh_multiply does that ?
|
||||
return True
|
||||
except:
|
||||
# dont waste bytes on error messages for hackers
|
||||
return False
|
||||
|
||||
async def kt_start_send(rx_pubkey):
|
||||
# a QR was scanned and it held a pubkey
|
||||
# they want to send to this guy, ask them what to send, etc
|
||||
msg = '''You can now teleport secrets. You can select from seed words, temporary keys,
|
||||
|
||||
if not valid_looking_pubkey(rx_pubkey): return
|
||||
|
||||
msg = '''You can now teleport secrets. You can select from seed words, temporary keys, \
|
||||
secure notes and passwords. \
|
||||
|
||||
WARNING: \
|
||||
The other COLDCARD will have full access to all Bitcoin controlled by these keys!
|
||||
WARNING: Receiver will have full access to all Bitcoin controlled by these keys!
|
||||
'''
|
||||
|
||||
ch = await ux_show_story(msg, title="Key Teleport: Send")
|
||||
|
||||
# TODO: pick what to send, somehow ...
|
||||
menu = SecretPickerMenu(rx_pubkey)
|
||||
|
||||
body = b'w'+ (b'A'*16)
|
||||
the_ux.push(menu)
|
||||
|
||||
async def kt_do_send(rx_pubkey, dtype, raw=None, obj=None):
|
||||
# Example: cleartext = b'w'+ (b'A'*16)
|
||||
cleartext = dtype.encode() + (raw or json.dumps(obj).encode())
|
||||
|
||||
# Pick and show noid key to sender
|
||||
noid_key = ngu.random.bytes(16)
|
||||
noid_key, txt = pick_noid_key()
|
||||
|
||||
msg = "Share passphrase with receiver, via some different channel:"
|
||||
msg += ux_render_words(bip39.b2a_words(noid_key).split())
|
||||
await ux_show_story(msg, 'Paranoid Key')
|
||||
msg = "Share this password with the receiver, via some different channel:"\
|
||||
"\n\n %s = %s\n\n" % (txt, ' '.join(txt))
|
||||
msg += "ENTER to view QR"
|
||||
|
||||
# throw away entropy
|
||||
# all new EC key
|
||||
my_keypair = ngu.secp256k1.keypair()
|
||||
|
||||
words = bip39.b2a_words(noid_key).split(' ')
|
||||
payload = encode_payload(my_keypair, rx_pubkey, noid_key, cleartext)
|
||||
|
||||
payload = encode_payload(my_keypair, rx_pubkey, noid_key, body)
|
||||
await tk_show_payload('S', payload, 'Teleport Password', cta='Show to Receiver', msg=msg)
|
||||
|
||||
await tk_show_payload('S', payload, None, 'Show to Receiver')
|
||||
from flow import goto_top_menu
|
||||
goto_top_menu()
|
||||
|
||||
def pick_noid_key():
|
||||
# pick an 40 bit password, shown as base32
|
||||
# - on rx, libngu base32 decoder will convert '018' into 'OLB'
|
||||
# - but a little tempted to removed vowels here?
|
||||
k = ngu.random.bytes(5)
|
||||
txt = b32encode(k).upper()
|
||||
|
||||
return k, txt
|
||||
|
||||
async def kt_decode_rx(is_psbt, payload):
|
||||
# we are getting data back from a sender, decode it.
|
||||
@ -159,45 +189,62 @@ async def kt_decode_rx(is_psbt, payload):
|
||||
rx_key = settings.get("ktrx")
|
||||
if not rx_key:
|
||||
await ux_show_story("Not expecting any teleports. You need to start over.")
|
||||
|
||||
await kt_start_rx() # help them to start over? idk maybe not.
|
||||
return
|
||||
|
||||
his_pubkey = payload[0:33]
|
||||
body = payload[33:]
|
||||
pair = ngu.secp256k1.keypair(a2b_hex(rx_key))
|
||||
|
||||
else:
|
||||
randint = payload[0:4]
|
||||
body = payload[4:]
|
||||
|
||||
# may need to iterate over a few wallets?
|
||||
|
||||
ses_key, body = await decode_step1(pair, his_pubkey, body)
|
||||
ses_key, body = decode_step1(pair, his_pubkey, body)
|
||||
|
||||
if not ses_key:
|
||||
await ux_show_story("QR code is damaged or incorrect.\n\n" + body, title="Decode Fail")
|
||||
# when ECDH fails, it's truncation or wrong RX key (due to sender using old rx key, etc)
|
||||
await ux_show_story("QR code is damaged, or was sent to a different user. "
|
||||
"Sender should start again.", title="Teleport Fail")
|
||||
return
|
||||
|
||||
from glob import dis
|
||||
while 1:
|
||||
# ask for noid key
|
||||
words = await seed_word_entry('Paranoid Key', 12, has_checksum=True)
|
||||
if not words:
|
||||
noid_key = b'\x5a' * 16
|
||||
noid_key = bip39.a2b_words(words)
|
||||
pw = await ux_input_text('', confirm_exit=False, hex_only=False, max_len=8,
|
||||
prompt='Teleport Password', min_len=8, b39_complete=False, scan_ok=False,
|
||||
placeholder='********', funct_keys=None, force_xy=None)
|
||||
|
||||
final = decode_step2(ses_key, noid_key, body)
|
||||
if final is not None:
|
||||
break
|
||||
dis.progress_bar_show(0)
|
||||
try:
|
||||
assert len(pw) == 8
|
||||
noid_key = b32decode(pw) # case insenstive, and smart about confused chars
|
||||
final = decode_step2(ses_key, noid_key, body)
|
||||
if final is not None:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
ch = await ux_show_story("Incorrect Paranoid Key. You can try again or CANCEL to stop.")
|
||||
ch = await ux_show_story(
|
||||
"Incorrect Teleport Password. You can try again or CANCEL to stop.")
|
||||
if ch == 'x': return
|
||||
# will ask again
|
||||
|
||||
await kt_accept_values(final[0].decode(), final[1:])
|
||||
# success w/ decoding. but maybe something goes wrong or they reject a confirm step
|
||||
# so keep the rx key alive still
|
||||
|
||||
await kt_accept_values(chr(final[0]), final[1:])
|
||||
|
||||
async def kt_accept_values(dtype, raw):
|
||||
# got the secret, decode it more
|
||||
# We got some secret, decode it more, and save it.
|
||||
'''
|
||||
- `w` - 12/18/24 words - 16/24/32 bytes follow
|
||||
- `m` - (one byte of length) + (up to 71 bytes) - BIP-32 raw master secret [rare]
|
||||
- `s` - secret, encoded per stash.py
|
||||
- `m` - (up to 72 bytes?) - BIP-32 raw master secret [rare]
|
||||
- `r` - raw XPRV mode - 64 bytes follow which are the chain code then master privkey
|
||||
- `x` - XPRV mode, full details - 4 bytes (XPRV) + base58 *decoded* binary-XPRV follows
|
||||
- `n` - one or many notes export (JSON array)
|
||||
@ -205,68 +252,74 @@ async def kt_accept_values(dtype, raw):
|
||||
- `p` - binary PSBT to be signed
|
||||
- `P` - a more-signed binary PSBT being returned back to sender
|
||||
'''
|
||||
from stash import SecretStash
|
||||
from chains import current_chain, slip32_deserialize
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
|
||||
enc = None
|
||||
meta = 'Teleported'
|
||||
origin = 'Teleported'
|
||||
label = None
|
||||
|
||||
if dtype == 's':
|
||||
# words / bip 32 master / xprv, etc
|
||||
enc = bytearray(72)
|
||||
enc[0:len(raw)] = raw
|
||||
|
||||
if dtype == 'w':
|
||||
# words.
|
||||
assert len(raw) in { 16, 24, 32 }
|
||||
enc = SecretStash.encode(seed_phrase=raw)
|
||||
elif dtype == 'm':
|
||||
enc = SecretStash.encode(master_secret=raw)
|
||||
elif dtype == 'r':
|
||||
assert len(raw) == 64
|
||||
enc = b'\x01' + raw
|
||||
elif dtype == 'x':
|
||||
# it's an XPRV, but in binary.. some extra data we throw away here; sigh
|
||||
|
||||
# XXX no way to send this .. but was thinking of address explorer
|
||||
txt = ngu.codecs.b58_encode(raw)
|
||||
node, ch, _, _ = slip32_deserialize(txt)
|
||||
assert ch.name == chains.current_chain.name, 'wrong chain'
|
||||
enc = SecretStash.encode(node=node)
|
||||
enc = stash.SecretStash.encode(node=node)
|
||||
|
||||
elif dtype in 'pP':
|
||||
# raw PSBT -- bigger
|
||||
from auth import sign_transaction
|
||||
psbt_len = len(raw)
|
||||
|
||||
# copy into PSRAM
|
||||
with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out:
|
||||
out.write(raw)
|
||||
|
||||
# this will take over UX w/ the signing process
|
||||
# This will take over UX w/ the signing process
|
||||
sign_transaction(psbt_len, flags=0x0)
|
||||
return
|
||||
|
||||
elif dtype in 'nv':
|
||||
# all are JSON things
|
||||
js = loads(raw)
|
||||
js = json.loads(raw)
|
||||
|
||||
if dtype == 'v':
|
||||
# one key export from a seed vault
|
||||
enc = a2b_hex(js[1])
|
||||
meta = js[2]
|
||||
# - watch for incompatibility here if we ever change VaultEntry
|
||||
from seed import VaultEntry
|
||||
rec = VaultEntry(*js)
|
||||
enc = deserialize_secret(rec.encoded)
|
||||
origin = rec.origin
|
||||
label = rec.label
|
||||
elif dtype == 'n':
|
||||
# secure note(s)
|
||||
from notes import import_from_json
|
||||
# import secure note(s)
|
||||
from notes import import_from_json, make_notes_menu, NoteContent
|
||||
|
||||
settings.remove_key("ktrx") # force new rx key after this point
|
||||
await import_from_json(dict(coldcard_notes=js))
|
||||
|
||||
await ux_dramatic_pause('Imported.', 3)
|
||||
await ux_dramatic_pause('Imported.', 2)
|
||||
|
||||
# TODO: force them into notes submenu so they see result?
|
||||
# force them into notes submenu so they can see result right away
|
||||
# - highlight to last note, which should be the just-added one(s)
|
||||
goto_top_menu()
|
||||
nm = await make_notes_menu()
|
||||
nm.goto_idx(NoteContent.count()-1)
|
||||
the_ux.push(nm)
|
||||
|
||||
return
|
||||
else:
|
||||
raise ValueError(dtype)
|
||||
|
||||
# key material is arriving; offer to use as main secret or tmp or seed vault
|
||||
# key material is arriving; offer to use as main secret, or tmp, or seed vault?
|
||||
assert enc
|
||||
#summary = SecretStash.summary(enc[0])
|
||||
|
||||
from flow import has_se_secrets, goto_top_menu
|
||||
from seed import set_ephemeral_seed, set_seed_value
|
||||
|
||||
if not has_se_secrets():
|
||||
@ -274,50 +327,60 @@ async def kt_accept_values(dtype, raw):
|
||||
set_seed_value(encoded=enc)
|
||||
ok = True
|
||||
else:
|
||||
ok = await set_ephemeral_seed(enc, meta=meta)
|
||||
ok = await set_ephemeral_seed(enc, origin=origin, label=label)
|
||||
|
||||
if ok:
|
||||
settings.remove_key("ktrx") # force new rx key after this point
|
||||
goto_top_menu()
|
||||
|
||||
def noid_stretch(session_key, noid_key):
|
||||
return ngu.hash.pbkdf2_sha512(session_key, noid_key, 5000)[0:32]
|
||||
|
||||
def encode_payload(my_keypair, his_pubkey, noid_key, body):
|
||||
# do all the encryption
|
||||
assert len(his_pubkey) == 33
|
||||
assert len(noid_key) == 16
|
||||
assert len(noid_key) == 5
|
||||
|
||||
session_key = my_keypair.ecdh_multiply(his_pubkey)
|
||||
|
||||
b1 = aes256ctr.new(session_key).cipher(body)
|
||||
b1 += ngu.hash.sha256s(b1)[-2:]
|
||||
# stretch noid key out -- will be slow
|
||||
pk = noid_stretch(session_key, noid_key)
|
||||
|
||||
b2 = aes256ctr.new(noid_key + session_key[16:]).cipher(b1)
|
||||
b2 += ngu.hash.sha256s(b2)[-2:]
|
||||
b1 = aes256ctr.new(pk).cipher(body)
|
||||
b1 += ngu.hash.sha256s(body)[-2:]
|
||||
|
||||
return my_keypair.pubkey().to_bytes(True) + b2
|
||||
b2 = aes256ctr.new(session_key).cipher(b1)
|
||||
b2 += ngu.hash.sha256s(b1)[-2:]
|
||||
|
||||
return my_keypair.pubkey().to_bytes() + b2
|
||||
|
||||
def decode_step1(my_keypair, his_pubkey, body):
|
||||
# Do ECDH and get out next layer of encryption
|
||||
# Do ECDH and remove top layer of encryption
|
||||
try:
|
||||
assert len(his_pubkey) == 33
|
||||
assert valid_looking_pubkey(his_pubkey)
|
||||
assert len(body) >= 10
|
||||
|
||||
session_key = my_keypair.ecdh_multiply(his_pubkey)
|
||||
|
||||
body = aes256ctr.new(session_key).cipher(payload[:-2])
|
||||
chk = sha256s(body)[-2:]
|
||||
assert chk == payload[-2:], 'first checksum'
|
||||
except Exception as exc:
|
||||
ln = problem_file_line(exc)
|
||||
return None, ln
|
||||
rv = aes256ctr.new(session_key).cipher(body[:-2])
|
||||
chk = ngu.hash.sha256s(rv)[-2:]
|
||||
|
||||
return session_key, body
|
||||
assert chk == body[-2:] # likely means wrong rx key, or truncation
|
||||
except:
|
||||
return None, None
|
||||
|
||||
return session_key, rv
|
||||
|
||||
def decode_step2(session_key, noid_key, body):
|
||||
tk = noid_key + session_key[16:]
|
||||
msg = aes256ctr.new(noid_key + session_key[16:]).cipher(body[:-2])
|
||||
chk = sha256(msg)[:-2]
|
||||
# After we have the noid key, can decode true payload
|
||||
assert len(noid_key) == 5
|
||||
|
||||
return msg if chk == msg[-2:] else None
|
||||
pk = noid_stretch(session_key, noid_key)
|
||||
|
||||
msg = aes256ctr.new(pk).cipher(body[:-2])
|
||||
chk = ngu.hash.sha256s(msg)[-2:]
|
||||
|
||||
return msg if chk == body[-2:] else None
|
||||
|
||||
|
||||
async def kt_incoming(type_code, payload):
|
||||
@ -335,6 +398,100 @@ async def kt_incoming(type_code, payload):
|
||||
# incoming PSBT!
|
||||
return await kt_decode_rx(True, payload)
|
||||
|
||||
|
||||
else:
|
||||
raise ValueError(type_code)
|
||||
|
||||
|
||||
class SecretPickerMenu(MenuSystem):
|
||||
def __init__(self, rx_pubkey):
|
||||
self.rx_pubkey = rx_pubkey
|
||||
|
||||
from flow import word_based_seed, is_tmp
|
||||
from stash import bip39_passphrase
|
||||
has_notes = bool(settings.get('secnap', False))
|
||||
has_ms = bool(settings.get('multisig', False))
|
||||
has_sv = bool(settings.get('seedvault', False))
|
||||
|
||||
# Q-only feature, so menu can be W I D E
|
||||
# - in increasing order of important / sensitivity!
|
||||
m = [
|
||||
MenuItem('Multisig PSBT for Signing', predicate=has_ms),
|
||||
MenuItem('Single Note / Password', predicate=has_notes, menu=self.pick_note_submenu),
|
||||
MenuItem('Export All Notes & Passwords', predicate=has_notes, f=self.picked_note),
|
||||
]
|
||||
|
||||
if has_sv:
|
||||
m.append( MenuItem('From Seed Vault', menu=self.pick_vault_submenu) )
|
||||
|
||||
if is_tmp():
|
||||
# tmp seed, or maybe bip39 is in effect
|
||||
# - all are the current master secret
|
||||
msg = 'Temp Secret (words)' if word_based_seed() else (
|
||||
'XPRV from Words+Passphrase' if bip39_passphrase else 'Temp XPRV Secret')
|
||||
|
||||
else:
|
||||
# real master secret
|
||||
msg = 'Master Seed Words' if word_based_seed() else 'Master XPRV'
|
||||
|
||||
m.append( MenuItem(msg, f=self.share_master_secret) )
|
||||
|
||||
super().__init__(m)
|
||||
|
||||
async def pick_vault_submenu(self, *a):
|
||||
# pick a secret from seed vault
|
||||
from seed import SeedVaultChooserMenu
|
||||
rec = await SeedVaultChooserMenu.pick()
|
||||
if rec:
|
||||
await kt_do_send(self.rx_pubkey, 'v', obj=list(rec))
|
||||
|
||||
async def pick_note_submenu(self, *a):
|
||||
# Make a submenu to select a single note/password
|
||||
from notes import NoteContent
|
||||
|
||||
rv = []
|
||||
for note in NoteContent.get_all():
|
||||
rv.append(MenuItem('%d: %s' % (note.idx+1, note.title), f=self.picked_note, arg=note))
|
||||
|
||||
return rv
|
||||
|
||||
async def picked_note(self, _, _2, item):
|
||||
# exporting note(s)
|
||||
from notes import NoteContent
|
||||
|
||||
if item.arg is None:
|
||||
# export all
|
||||
body = [n.serialize() for n in NoteContent.get_all()]
|
||||
else:
|
||||
# single note/password
|
||||
body = [item.arg.serialize()]
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 'n', obj=body)
|
||||
|
||||
async def share_master_secret(self, _, _2, item):
|
||||
# altho menu items look different we are sharing same thing:
|
||||
# - up to 72 bytes from secure elements
|
||||
from stash import SensitiveValues, SecretStash, blank_object
|
||||
|
||||
with stash.SensitiveValues(bypass_tmp=False, enforce_delta=True) as sv:
|
||||
raw = bytearray(sv.secret)
|
||||
xfp = xfp2str(sv.get_xfp())
|
||||
|
||||
# rtrim zeros
|
||||
while raw[-1] == 0:
|
||||
raw = raw[0:-1]
|
||||
|
||||
summary = SecretStash.summary(raw[0])
|
||||
|
||||
from pincodes import pa
|
||||
scale = 'your MASTER secret' if not pa.tmp_value else 'a temporary secret'
|
||||
|
||||
msg = "Sharing %s [%s] (%s)." % (scale, xfp, summary)
|
||||
msg += "\n\nWARNING: Allows full control over all associated Bitcoin!"
|
||||
|
||||
if not await ux_confirm(msg):
|
||||
blank_object(raw)
|
||||
return
|
||||
|
||||
await kt_do_send(self.rx_pubkey, 's', raw=raw)
|
||||
|
||||
# EOF
|
||||
|
||||
@ -776,7 +776,7 @@ normal operation.''')
|
||||
|
||||
# switch over to new secret!
|
||||
dis.fullscreen("Applying...")
|
||||
await set_ephemeral_seed(encoded, meta=name)
|
||||
await set_ephemeral_seed(encoded, origin=name)
|
||||
goto_top_menu()
|
||||
|
||||
async def countdown_details(self, m, l, item):
|
||||
|
||||
@ -551,10 +551,11 @@ def chunk_writer(fd, body):
|
||||
dis.progress_bar_show(1)
|
||||
|
||||
|
||||
def pad_raw_secret(text_sec_str):
|
||||
def deserialize_secret(text_sec_str):
|
||||
# Chip can hold 72-bytes as a secret
|
||||
# every secret has 0th byte as marker
|
||||
# then secret and padded to zero to AE_SECRET_LEN
|
||||
# - has 0th byte as marker, secret and zero padding to AE_SECRET_LEN
|
||||
# - also does hex to binary conversion
|
||||
# - converse of: SecretStash.storage_serialize()
|
||||
from pincodes import AE_SECRET_LEN
|
||||
|
||||
raw = bytearray(AE_SECRET_LEN)
|
||||
|
||||
@ -8,11 +8,11 @@
|
||||
import ngu, bip39, version
|
||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause
|
||||
from ux import show_qr_code, ux_render_words, OK
|
||||
from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed
|
||||
from seed import word_quiz, WordNestMenu, set_seed_value, set_ephemeral_seed, seed_vault_iter
|
||||
from glob import settings
|
||||
from menu import MenuSystem, MenuItem
|
||||
from actions import goto_top_menu
|
||||
from utils import encode_seed_qr, pad_raw_secret, xor
|
||||
from utils import encode_seed_qr, deserialize_secret, xor
|
||||
from charcodes import KEY_QR
|
||||
from stash import SecretStash, blank_object, SensitiveValues, numwords_to_len, len_to_numwords
|
||||
|
||||
@ -203,13 +203,11 @@ async def xor_all_done(data):
|
||||
# ))
|
||||
# for i in enc_parts
|
||||
# ]
|
||||
await set_ephemeral_seed(
|
||||
enc,
|
||||
meta='SeedXOR(%d parts, check: "%s")' % (
|
||||
num_parts, chk_word
|
||||
)
|
||||
)
|
||||
await set_ephemeral_seed(enc,
|
||||
origin='SeedXOR(%d parts, check: "%s")' % (num_parts, chk_word))
|
||||
|
||||
goto_top_menu()
|
||||
|
||||
break
|
||||
|
||||
class XORWordNestMenu(WordNestMenu):
|
||||
@ -284,14 +282,14 @@ or press (2) for 18 words XOR.''' % OK, escape="12")
|
||||
# Add from Seed Vault?
|
||||
# filter only those that are correct length and type from seed vault
|
||||
opt = []
|
||||
for i, (xfp_str, hex_str, _, _) in enumerate(settings.master_get("seeds", [])):
|
||||
raw = pad_raw_secret(hex_str)
|
||||
for i, rec in seed_vault_iter():
|
||||
raw = deserialize_secret(rec.encoded)
|
||||
|
||||
nw = SecretStash.is_words(raw)
|
||||
if nw and nw == desired_num_words:
|
||||
# it is words, and right length
|
||||
sk = SecretStash.decode_words(raw, bin_mode=True)
|
||||
opt.append((i, xfp_str, sk))
|
||||
opt.append((i, rec.xfp, sk))
|
||||
|
||||
blank_object(raw)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user