Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57f3ae86f8 | ||
|
|
24ce083aae | ||
|
|
f4a03d0fd9 | ||
|
|
e128161ada | ||
|
|
324e2d29c3 | ||
|
|
07d31cc68d |
96
docs/microsd-2fa.md
Normal file
96
docs/microsd-2fa.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
# MicroSD as a Second Factor for Login
|
||||||
|
|
||||||
|
When enabled, this feature requires a specially prepared MicroSD
|
||||||
|
card to be inserted during login process. After correct PIN is
|
||||||
|
provided, if card slot is empty or unknown card present, the seed
|
||||||
|
is wiped.
|
||||||
|
|
||||||
|
## How it Works
|
||||||
|
|
||||||
|
To "enroll" a card, a small encrypted file is written to the card.
|
||||||
|
|
||||||
|
During login, after the correct (true) PIN is entered, we use
|
||||||
|
the master secret to construct an AES key which is used to decrypt
|
||||||
|
the file found on the card. If the file is JSON and contains a nonce,
|
||||||
|
we check that in our list of acceptable cards.
|
||||||
|
|
||||||
|
The AES key includes the master secret and also a hash of the
|
||||||
|
unique serial number of the card, retrieved using low-level
|
||||||
|
protocols. This prevents moving the file to another card.
|
||||||
|
|
||||||
|
To allow the same card to unlock multiple Coldcards, we write the
|
||||||
|
file using a filename derived from the serial number of the Coldcard
|
||||||
|
(hashed). Thus there could be a number of 2FA-enabling files on a
|
||||||
|
single card.
|
||||||
|
|
||||||
|
The file name starts with a dot, and has extension `.2fa`. Your
|
||||||
|
tools may or may not hide it from you based on Unix filename
|
||||||
|
conventions. Reformating the card will certainly remove this file,
|
||||||
|
so keep that in mind when managing your "special" cards.
|
||||||
|
|
||||||
|
## Menu Settings
|
||||||
|
|
||||||
|
See menu in: `Settings -> Login Settings -> MicroSD 2FA`
|
||||||
|
|
||||||
|
The option is enabled only once the main secret is picked. It cannot
|
||||||
|
be used with ephemeral seeds, as that secret will not be in effect
|
||||||
|
during boot time.
|
||||||
|
|
||||||
|
The menu initially contains only "Add Card". Once one or more
|
||||||
|
cards are enabled (and the feature is activated), addition
|
||||||
|
options appear: "Check Card" and "Remove Card #N" for each
|
||||||
|
enrolled card.
|
||||||
|
|
||||||
|
"Check Card" validates the card inserted and indicates if it would
|
||||||
|
be accepted or not.
|
||||||
|
|
||||||
|
Use "Remove Card #N" is remove cards from the system. When the last
|
||||||
|
card is removed, the feature is disabled and no card will be required
|
||||||
|
for login.
|
||||||
|
|
||||||
|
## During Login
|
||||||
|
|
||||||
|
After the PIN is entered, and if it is the true PIN (or the main
|
||||||
|
code thinks it is, in Delta Mode or Duress Wallet cases) the main
|
||||||
|
settings are read. After this point, if there are one or more card
|
||||||
|
enrolled, then the check is performed. If the slot is empty or
|
||||||
|
the card fails the check, a fast wipe of the seed is done and shown
|
||||||
|
on screen. The memory is wipe and system stops. You must power cycle
|
||||||
|
to continue.
|
||||||
|
|
||||||
|
## Tricky Thinking
|
||||||
|
|
||||||
|
Because settings are encrypted by the master seed, if you have a
|
||||||
|
duress wallet, it could have required cards set as well. Generally,
|
||||||
|
we do not see a good use for this, and assume that typically only
|
||||||
|
the "true" PIN will have required cards associated with it. Remember
|
||||||
|
any Trick PIN can wipe the seed directly.
|
||||||
|
|
||||||
|
In Delta Mode, the usual card policy is in effect. However, if you
|
||||||
|
are relying on this 2FA feature to wipe the seed in a case of duress,
|
||||||
|
there doesn't seem to be any need for Delta Mode.
|
||||||
|
|
||||||
|
## Duress Defenses
|
||||||
|
|
||||||
|
We recommend simply keeping no card in your Coldcard once activating
|
||||||
|
this feature. Your attacker, or yourself under duress, will login
|
||||||
|
normally and trigger this defense without you taking any explicit
|
||||||
|
action.
|
||||||
|
|
||||||
|
If you were being forced to prepare a PSBT under duress, you can
|
||||||
|
choose which SD card to use (so pick a normal one, which isn't
|
||||||
|
enrolled) and you may also have a chance to clear your card of the
|
||||||
|
special file. Either way would be an opportunity to ensure the
|
||||||
|
automatic wipe occurs, even as you comply and provide the PIN code.
|
||||||
|
|
||||||
|
Your enrolled SD cards can also be stored at another location away
|
||||||
|
from your Coldcard. This could be a bank safety deposit box, since
|
||||||
|
it contains no sensitive data.
|
||||||
|
|
||||||
|
If you are closely surveilled when logging and using your Coldcard,
|
||||||
|
the PIN code might already be known to your attacker. However, there
|
||||||
|
is no indication on the screen during a normal (successful) login
|
||||||
|
that this feature is in effect, so they would not know if the SD
|
||||||
|
card was inserted by chance or necessity.
|
||||||
|
|
||||||
@ -1,22 +1,28 @@
|
|||||||
## 5.1.0 - 2023-02-08
|
## 5.1.0 - 2023-02-XX
|
||||||
|
|
||||||
|
- New Feature: "MicroSD card as Second Factor". Specially marked MicroSD card must be
|
||||||
|
already inserted when (true) PIN is entered, or else seed is wiped. Add, remove and check
|
||||||
|
cards in menu: `Settings -> Login Settings -> MicroSD 2FA`
|
||||||
- New Feature: Single signature wallet generic descriptor export
|
- New Feature: Single signature wallet generic descriptor export
|
||||||
`Advanced -> Export Wallet -> Descriptor`. Both new format with internal/external
|
`Advanced -> Export Wallet -> Descriptor`. Both new format with internal/external
|
||||||
in one descriptor `<0;1>` and standard with two descriptors are supported.
|
in one descriptor `<0;1>` and standard with two descriptors are supported.
|
||||||
- Enhancement: Add ability to import multisig wallet via Virtual Disk.
|
- Address Explorer:
|
||||||
- Enhancement: Add ability to import extended private key via Virtual Disk and via NFC.
|
- Enhancement: Application-specific derivation paths in `Address Explorer -> Applications`
|
||||||
- Enhancement: Import seed in compact/truncated form (just 3-4 letters of each seed word).
|
- Bugfix: Change value was ignored when generating addresses file
|
||||||
- Enhancement: Application-specific address/derivation paths in `Address Explorer -> Applications`
|
- Import Enhancements:
|
||||||
- Enhancement: Samourai POST-MIX and PRE-MIX descriptor export options added to `Export Wallet`
|
- Add import multisig wallet via Virtual Disk
|
||||||
- Enhancement: Lily Wallet export option added to `Export Wallet`
|
- Add import extended private key via Virtual Disk and via NFC
|
||||||
- Enhancement: Add ability to export all supported wallets via NFC (instead of SD card only)
|
- Import seed in compact/truncated form (just 3-4 letters of each seed word)
|
||||||
|
- Export Enhancements:
|
||||||
|
- Samourai POST-MIX and PRE-MIX descriptor export options added
|
||||||
|
- Lily Wallet added
|
||||||
|
- Ability to export all supported wallets via NFC (instead of SD card only)
|
||||||
|
- Change electrum export file name from 'new-wallet.json' to 'new-electrum.json'
|
||||||
|
- Allow export of Wasabi skeleton for Bitcoin Regtest.
|
||||||
- Enhancement: During seed generation from dice rolls, enforce at least 50 rolls
|
- Enhancement: During seed generation from dice rolls, enforce at least 50 rolls
|
||||||
for 12 word seeds, and 99 rolls for 24 word seeds. Statistical distribution check
|
for 12 word seeds, and 99 rolls for 24 word seeds. Statistical distribution check
|
||||||
added to prevent users from generating low-entropy seeds by rolling same value repeatedly.
|
added to prevent users from generating low-entropy seeds by rolling same value repeatedly.
|
||||||
- Enhancement: Change electrum export file name from 'new-wallet.json' to 'new-electrum.json'
|
|
||||||
- Bugfix: Change value was ignored when generating addresses file from address explorer.
|
|
||||||
- Bugfix: Offer import/export from/to Virtual Disk in UI even if SD Card is inserted.
|
- Bugfix: Offer import/export from/to Virtual Disk in UI even if SD Card is inserted.
|
||||||
- Bugfix: Allow export of Wasabi skeleton for Bitcoin Regtest.
|
|
||||||
- Docs: Add `docs/rolls12.py` script for verifying dice rolls math for 12 word seeds.
|
- Docs: Add `docs/rolls12.py` script for verifying dice rolls math for 12 word seeds.
|
||||||
|
|
||||||
## 5.0.7 - 2022-10-05
|
## 5.0.7 - 2022-10-05
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
#
|
#
|
||||||
# Every function here is called directly by a menu item. They should all be async.
|
# Every function here is called directly by a menu item. They should all be async.
|
||||||
#
|
#
|
||||||
import ckcc, pyb, version, uasyncio
|
import ckcc, pyb, version, uasyncio, sys
|
||||||
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted, ux_enter_bip32_index, ux_input_text
|
from ux import ux_show_story, the_ux, ux_confirm, ux_dramatic_pause, ux_aborted, ux_enter_bip32_index, ux_input_text
|
||||||
from utils import imported, pretty_short_delay, problem_file_line, import_prompt_builder
|
from utils import imported, pretty_short_delay, problem_file_line, import_prompt_builder
|
||||||
from uasyncio import sleep_ms
|
from uasyncio import sleep_ms
|
||||||
@ -855,7 +855,7 @@ async def start_login_sequence():
|
|||||||
|
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
# Robustness: any logic errors/bugs in above will brick the Coldcard
|
# Robustness: any logic errors/bugs in above will brick the Coldcard
|
||||||
# even for legit owner, since they can't login. Try to recover, when it's
|
# even for legit owner, since they can't login. To try to recover, when it's
|
||||||
# safe to do so. Remember the bootrom checks PIN on every access to
|
# safe to do so. Remember the bootrom checks PIN on every access to
|
||||||
# the secret, so "letting" them past this point is harmless if they don't know
|
# the secret, so "letting" them past this point is harmless if they don't know
|
||||||
# the true pin.
|
# the true pin.
|
||||||
@ -863,7 +863,6 @@ async def start_login_sequence():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
print("Bug recovery!")
|
print("Bug recovery!")
|
||||||
import sys
|
|
||||||
sys.print_exception(exc)
|
sys.print_exception(exc)
|
||||||
|
|
||||||
# Successful login...
|
# Successful login...
|
||||||
@ -879,6 +878,14 @@ async def start_login_sequence():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Maybe insist on the "right" microSD being already installed?
|
||||||
|
try:
|
||||||
|
from pwsave import MicroSD2FA
|
||||||
|
MicroSD2FA.enforce_policy()
|
||||||
|
except BaseException as exc:
|
||||||
|
# robustness: keep going!
|
||||||
|
sys.print_exception(exc)
|
||||||
|
|
||||||
# implement idle timeout now that we are logged-in
|
# implement idle timeout now that we are logged-in
|
||||||
from imptask import IMPT
|
from imptask import IMPT
|
||||||
IMPT.start_task('idle', idle_logout())
|
IMPT.start_task('idle', idle_logout())
|
||||||
@ -2088,5 +2095,16 @@ async def change_which_chain(name):
|
|||||||
# no secrets yet, not an error
|
# no secrets yet, not an error
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def microsd_2fa(*a):
|
||||||
|
# Feature: enforce special MicroSD being inserted at login time (a 2FA)
|
||||||
|
from pwsave import MicroSD2FA
|
||||||
|
|
||||||
|
if not settings.get('sd2fa'):
|
||||||
|
ch = await ux_show_story('''When enabled, this feature requires a specially prepared MicroSD card to be inserted during login process. After correct PIN is provided, if card slot is empty or unknown card present, the seed is wiped.''')
|
||||||
|
|
||||||
|
if ch != 'y':
|
||||||
|
return
|
||||||
|
|
||||||
|
return MicroSD2FA.menu()
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -87,6 +87,10 @@ def nfc_enabled():
|
|||||||
def vdisk_enabled():
|
def vdisk_enabled():
|
||||||
return bool(settings.get('vidsk', 0))
|
return bool(settings.get('vidsk', 0))
|
||||||
|
|
||||||
|
def se2_and_real_secret():
|
||||||
|
from pincodes import pa
|
||||||
|
return version.has_se2 and (not pa.is_secret_blank()) and (not pa.tmp_value)
|
||||||
|
|
||||||
|
|
||||||
HWTogglesMenu = [
|
HWTogglesMenu = [
|
||||||
ToggleMenuItem('USB Port', 'du', ['Default On', 'Disable USB'], invert=True,
|
ToggleMenuItem('USB Port', 'du', ['Default On', 'Disable USB'], invert=True,
|
||||||
@ -114,6 +118,7 @@ LoginPrefsMenu = [
|
|||||||
MenuItem('Scramble Keypad', f=pick_scramble),
|
MenuItem('Scramble Keypad', f=pick_scramble),
|
||||||
MenuItem('Kill Key', f=pick_killkey, predicate=lambda: version.has_se2),
|
MenuItem('Kill Key', f=pick_killkey, predicate=lambda: version.has_se2),
|
||||||
MenuItem('Login Countdown', chooser=countdown_chooser),
|
MenuItem('Login Countdown', chooser=countdown_chooser),
|
||||||
|
MenuItem('MicroSD 2FA', menu=microsd_2fa, predicate=se2_and_real_secret),
|
||||||
MenuItem('Test Login Now', f=login_now, arg=1),
|
MenuItem('Test Login Now', f=login_now, arg=1),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,7 @@ from glob import PSRAM
|
|||||||
# emu = (bool) if set, enables the USB Keyboard emulation (BIP-85 password entry)
|
# emu = (bool) if set, enables the USB Keyboard emulation (BIP-85 password entry)
|
||||||
# wa = (bool) if set, enables menu wraparound
|
# wa = (bool) if set, enables menu wraparound
|
||||||
# hsmcmd = (bool) if set, enables all user management and hsm-only USB commands
|
# hsmcmd = (bool) if set, enables all user management and hsm-only USB commands
|
||||||
|
# sd2fa = (list of strings): track which SD card is needed for login
|
||||||
# Stored w/ key=00 for access before login
|
# Stored w/ key=00 for access before login
|
||||||
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
# _skip_pin = hard code a PIN value (dangerous, only for debug)
|
||||||
# nick = optional nickname for this coldcard (personalization)
|
# nick = optional nickname for this coldcard (personalization)
|
||||||
|
|||||||
223
shared/pwsave.py
223
shared/pwsave.py
@ -2,8 +2,9 @@
|
|||||||
#
|
#
|
||||||
# pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired)
|
# pwsave.py - Save bip39 passphrases into encrypted file on MicroSD (if desired)
|
||||||
#
|
#
|
||||||
import sys, stash, ujson, os, ngu
|
import sys, stash, ujson, os, ngu, pyb
|
||||||
from files import CardSlot, CardMissingError, needs_microsd
|
from files import CardSlot, CardMissingError, needs_microsd
|
||||||
|
from ux import ux_dramatic_pause, ux_confirm, ux_show_story
|
||||||
|
|
||||||
class PassphraseSaver:
|
class PassphraseSaver:
|
||||||
# Encrypts BIP-39 passphrase very carefully, and appends
|
# Encrypts BIP-39 passphrase very carefully, and appends
|
||||||
@ -16,9 +17,10 @@ class PassphraseSaver:
|
|||||||
# - some very minor obscurity, but we aren't relying on that.
|
# - some very minor obscurity, but we aren't relying on that.
|
||||||
return card.get_sd_root() + '/.tmp.tmp'
|
return card.get_sd_root() + '/.tmp.tmp'
|
||||||
|
|
||||||
def _calc_key(self, card):
|
def _calc_key(self, card, force=False):
|
||||||
# calculate the key to be used.
|
# calculate the key to be used.
|
||||||
if getattr(self, 'key', None): return
|
if not force and getattr(self, 'key', None):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
salt = card.get_id_hash()
|
salt = card.get_id_hash()
|
||||||
@ -35,16 +37,20 @@ class PassphraseSaver:
|
|||||||
decrypt = ngu.aes.CTR(self.key)
|
decrypt = ngu.aes.CTR(self.key)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg = open(self.filename(card), 'rb').read()
|
fname = self.filename(card)
|
||||||
|
msg = open(fname, 'rb').read()
|
||||||
txt = decrypt.cipher(msg)
|
txt = decrypt.cipher(msg)
|
||||||
|
|
||||||
return ujson.loads(txt)
|
return ujson.loads(txt)
|
||||||
|
except OSError:
|
||||||
|
#print('missing? ' + fname)
|
||||||
|
return []
|
||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def append(self, xfp, bip39pw):
|
async def append(self, xfp, bip39pw):
|
||||||
# encrypt and save; always appends.
|
# encrypt and save; always appends.
|
||||||
from ux import ux_dramatic_pause
|
|
||||||
from glob import dis
|
from glob import dis
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
@ -79,7 +85,6 @@ class PassphraseSaver:
|
|||||||
from actions import goto_top_menu
|
from actions import goto_top_menu
|
||||||
from ux import ux_show_story
|
from ux import ux_show_story
|
||||||
from seed import set_bip39_passphrase
|
from seed import set_bip39_passphrase
|
||||||
import pyb
|
|
||||||
|
|
||||||
# Very quick check for card not present case.
|
# Very quick check for card not present case.
|
||||||
if not pyb.SDCard().present():
|
if not pyb.SDCard().present():
|
||||||
@ -143,5 +148,211 @@ class PassphraseSaver:
|
|||||||
|
|
||||||
|
|
||||||
return MenuSystem((MenuItem(label or '(empty)', f=doit, arg=pw) for pw, label in zip(pws, parts)))
|
return MenuSystem((MenuItem(label or '(empty)', f=doit, arg=pw) for pw, label in zip(pws, parts)))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Support for using MicroSD as second factor to the login PIN.
|
||||||
|
#
|
||||||
|
|
||||||
|
class MicroSD2FA(PassphraseSaver):
|
||||||
|
def filename(self, card):
|
||||||
|
# Construct actual filename to use.
|
||||||
|
# - want to support same card authorizing multiple CC, so cant be fixed filename
|
||||||
|
# - dont want to search tho, so should be deterministic
|
||||||
|
# - serial number of CC is nearly public but hmac anyway
|
||||||
|
# - if this file was written from a trick pin situation, it would have
|
||||||
|
# correct filename but contents would not decrypt since AES key is based off seed
|
||||||
|
import version
|
||||||
|
from utils import B2A
|
||||||
|
|
||||||
|
k = ngu.hash.sha256s(version.serial_number())
|
||||||
|
h = ngu.hmac.hmac_sha256(k, b'silly?')
|
||||||
|
|
||||||
|
return card.get_sd_root() + '/.%s.2fa' % B2A(h[0:8])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_nonces(cls):
|
||||||
|
# this is the only setting: list of nonce values we have saved to various cards
|
||||||
|
from glob import settings
|
||||||
|
return settings.get('sd2fa') or []
|
||||||
|
|
||||||
|
def read_card(self):
|
||||||
|
# Read the data, if any, and if decrypted correctly
|
||||||
|
|
||||||
|
# Read file, decrypt and make a menu to show; OR return None
|
||||||
|
# if any error hit.
|
||||||
|
try:
|
||||||
|
with CardSlot() as card:
|
||||||
|
self._calc_key(card, force=True)
|
||||||
|
if not self.key: return None
|
||||||
|
|
||||||
|
data = self._read(card)
|
||||||
|
if not data: return None
|
||||||
|
except CardMissingError:
|
||||||
|
# late fail
|
||||||
|
return None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enforce_policy(cls):
|
||||||
|
# If feature enabled, and if so check authorized card is inserted right now.
|
||||||
|
nonces = cls.get_nonces()
|
||||||
|
if not nonces:
|
||||||
|
# feature not in use, no problem
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
ok = cls.authorized_card_present(nonces)
|
||||||
|
assert ok == True
|
||||||
|
except:
|
||||||
|
# die. wrong
|
||||||
|
import callgate
|
||||||
|
callgate.fast_wipe(silent=False)
|
||||||
|
|
||||||
|
# proceed w/o any notice
|
||||||
|
return
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authorized_card_present(cls, nonces):
|
||||||
|
# Check if good card present
|
||||||
|
|
||||||
|
if not pyb.SDCard().present():
|
||||||
|
# no card present, so nope
|
||||||
|
return False
|
||||||
|
|
||||||
|
s = cls()
|
||||||
|
got = s.read_card()
|
||||||
|
if not got:
|
||||||
|
# garbage seen, missing file, etc => fail
|
||||||
|
#print('2fa file decrypt fail')
|
||||||
|
return False
|
||||||
|
#print(repr(got))
|
||||||
|
#print(repr(nonces))
|
||||||
|
|
||||||
|
# check it is in the list of authorized cards
|
||||||
|
return (got['nonce'] in nonces)
|
||||||
|
|
||||||
|
async def enroll(self):
|
||||||
|
# Write little file, update our settings to allow this card to auth.
|
||||||
|
from utils import B2A
|
||||||
|
from glob import dis, settings
|
||||||
|
|
||||||
|
nonce = B2A(ngu.random.bytes(8))
|
||||||
|
|
||||||
|
v = list(self.get_nonces())
|
||||||
|
|
||||||
|
# encrypt and save; always appends.
|
||||||
|
|
||||||
|
dis.fullscreen('Saving...')
|
||||||
|
|
||||||
|
try:
|
||||||
|
with CardSlot() as card:
|
||||||
|
self._calc_key(card, force=True)
|
||||||
|
|
||||||
|
data = dict(nonce=nonce)
|
||||||
|
|
||||||
|
encrypt = ngu.aes.CTR(self.key)
|
||||||
|
msg = encrypt.cipher(ujson.dumps(data))
|
||||||
|
|
||||||
|
with open(self.filename(card), 'wb') as fd:
|
||||||
|
fd.write(msg)
|
||||||
|
|
||||||
|
# update setting as well
|
||||||
|
v.append(nonce)
|
||||||
|
settings.set('sd2fa', v)
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
await ux_dramatic_pause("Saved.", 1)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
except CardMissingError:
|
||||||
|
return await needs_microsd()
|
||||||
|
|
||||||
|
async def remove(self, nonce):
|
||||||
|
# remove indicated nonce from records
|
||||||
|
# - delete file if present and found, but ok if missing
|
||||||
|
from glob import dis, settings
|
||||||
|
|
||||||
|
v = self.get_nonces()
|
||||||
|
assert nonce in v, 'missing card nonce'
|
||||||
|
v2 = [i for i in v if i != nonce]
|
||||||
|
if not v2:
|
||||||
|
settings.remove_key('sd2fa')
|
||||||
|
else:
|
||||||
|
settings.set('sd2fa', v2)
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with CardSlot() as card:
|
||||||
|
fn = self.filename(card)
|
||||||
|
os.remove(fn)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def menu(cls):
|
||||||
|
# menu contents needed for current state
|
||||||
|
from menu import MenuItem
|
||||||
|
|
||||||
|
existing = cls.get_nonces()
|
||||||
|
menu = []
|
||||||
|
|
||||||
|
menu.append(MenuItem("Add Card", f=cls.menu_enroll, arg=len(existing)))
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
menu.append(MenuItem("Check Card", f=cls.menu_check_card))
|
||||||
|
|
||||||
|
for n, card_nonce in enumerate(existing):
|
||||||
|
menu.append(MenuItem("Remove Card #%d" % (n+1), f=cls.menu_edit, arg=card_nonce))
|
||||||
|
|
||||||
|
return menu
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def menu_check_card(cls, *a):
|
||||||
|
|
||||||
|
ok = cls.authorized_card_present(cls.get_nonces())
|
||||||
|
if not ok:
|
||||||
|
await ux_show_story("This card would NOT be accepted during login.", title="FAIL")
|
||||||
|
else:
|
||||||
|
await ux_show_story("This card is enrolled and would be accepted during login.", title="PASS")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def menu_enroll(cls, menu, label, item):
|
||||||
|
from files import _is_ejected
|
||||||
|
|
||||||
|
count = item.arg
|
||||||
|
|
||||||
|
if _is_ejected():
|
||||||
|
return await needs_microsd()
|
||||||
|
|
||||||
|
# careful: if they re-enrolled same card twice, confusion will result
|
||||||
|
if count:
|
||||||
|
ok = cls.authorized_card_present(cls.get_nonces())
|
||||||
|
if ok:
|
||||||
|
await ux_show_story("Need a different MicroSD card. "
|
||||||
|
"This card would already be accepted.")
|
||||||
|
return
|
||||||
|
|
||||||
|
ctx = 'this card or one of the others' if count >= 1 else 'it'
|
||||||
|
|
||||||
|
ok = await ux_confirm("Add this card to authorized set? Going forward %s must be present during login process or the seed will be wiped!" % ctx)
|
||||||
|
|
||||||
|
|
||||||
|
await cls().enroll()
|
||||||
|
|
||||||
|
menu.replace_items(cls.menu())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def menu_edit(cls, menu, label, item):
|
||||||
|
# only allowing delete for now... could show details or something
|
||||||
|
ok = await ux_confirm("Remove this card from authorized set?")
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
# delete magic file if we can, but more importantly our nonce
|
||||||
|
await cls().remove(item.arg)
|
||||||
|
|
||||||
|
menu.replace_items(cls.menu())
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from glob import settings
|
|||||||
# accepting strings and strings, returning bytes when decoding, str when encoding (ie. correct)
|
# accepting strings and strings, returning bytes when decoding, str when encoding (ie. correct)
|
||||||
b32encode = ngu.codecs.b32_encode
|
b32encode = ngu.codecs.b32_encode
|
||||||
b32decode = ngu.codecs.b32_decode
|
b32decode = ngu.codecs.b32_decode
|
||||||
|
|
||||||
hmac_sha256 = ngu.hmac.hmac_sha256
|
hmac_sha256 = ngu.hmac.hmac_sha256
|
||||||
|
|
||||||
# to keep menus and such to a reasonable size
|
# to keep menus and such to a reasonable size
|
||||||
|
|||||||
@ -122,8 +122,8 @@ cs = files.CardSlot().__enter__(); \
|
|||||||
p=PassphraseSaver(); p._calc_key(cs); RV.write(b2a_hex(p.key)); cs.__exit__()''')
|
p=PassphraseSaver(); p._calc_key(cs); RV.write(b2a_hex(p.key)); cs.__exit__()''')
|
||||||
|
|
||||||
assert len(key) == 64
|
assert len(key) == 64
|
||||||
#assert key == '234af2aa2ab43af83667dfc6e11d08223e0f486ef34539b41a045dd9eb3ea664'
|
|
||||||
|
|
||||||
|
# recalc what it should be
|
||||||
from pycoin.key.BIP32Node import BIP32Node
|
from pycoin.key.BIP32Node import BIP32Node
|
||||||
from pycoin.encoding import from_bytes_32, to_bytes_32
|
from pycoin.encoding import from_bytes_32, to_bytes_32
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
@ -141,7 +141,7 @@ p=PassphraseSaver(); p._calc_key(cs); RV.write(b2a_hex(p.key)); cs.__exit__()'''
|
|||||||
|
|
||||||
assert expect == key
|
assert expect == key
|
||||||
|
|
||||||
# check that key works for decrypt / that the file was actually encrypted
|
# check that key works for decrypt and that the file was actually encrypted
|
||||||
|
|
||||||
with open(SIM_FNAME, 'rb') as fd:
|
with open(SIM_FNAME, 'rb') as fd:
|
||||||
raw = fd.read()
|
raw = fd.read()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user