diff --git a/shared/auth.py b/shared/auth.py index 8f5b451f..fedf4896 100644 --- a/shared/auth.py +++ b/shared/auth.py @@ -791,7 +791,8 @@ class ApproveTransaction(UserAuthorizedAction): async def interact(self): # Prompt user w/ details and get approval from glob import dis, hsm_active - from ccc import CCCFeature, PolicyViolationException + from ccc import CCCFeature + from exceptions import CCCPolicyViolationError # step 1: parse PSBT from PSRAM into in-memory objects. @@ -826,23 +827,12 @@ class ApproveTransaction(UserAuthorizedAction): dis.progress_bar_show(0.85) - if CCCFeature.is_enabled(): - if self.psbt.active_multisig and (ccc_c_xfp in self.psbt.active_multisig.xfp_paths): - CCCFeature.validate_tx(self.psbt) - self.psbt.sign_it(CCCFeature.get_encoded_secret(), ccc_c_xfp) - except FraudulentChangeOutput as exc: print('FraudulentChangeOutput: ' + exc.args[0]) return await self.failure(exc.args[0], title='Change Fraud') except FatalPSBTIssue as exc: print('FatalPSBTIssue: ' + exc.args[0]) return await self.failure(exc.args[0]) - except PolicyViolationException as exc: - ch = await ux_show_story( - exc.args[0]+"\n\n Would you like to sign with A key?", - title='Policy Violation') - if ch != "y": - return except BaseException as exc: del self.psbt gc.collect() @@ -855,6 +845,10 @@ class ApproveTransaction(UserAuthorizedAction): return await self.failure(msg, exc) + # early test for spending policy; not an error if violates policy + # - might add warnings + could_ccc_sign, needs_2fa = CCCFeature.could_sign(self.psbt) + # step 2: figure out what we are approving, so we can get sign-off # - outputs, amounts # - fee @@ -954,7 +948,7 @@ class ApproveTransaction(UserAuthorizedAction): return await self.failure(msg) if ch not in 'yb': - # they don't want to! + # they don't want to sign! self.refused = True await ux_dramatic_pause("Refused.", 1) @@ -964,11 +958,27 @@ class ApproveTransaction(UserAuthorizedAction): self.done() return + if needs_2fa and could_ccc_sign: + # They still need to pass web2fa challenge (but it meets other specs ok) + try: + await CCCFeature.web2fa_challenge() + except: + could_ccc_sign = False + ch = await ux_show_story("Will not add CCC signature. Proceed anyway?") + if ch != 'y': + return await self.failure("2FA Failed") + # do the actual signing. try: dis.fullscreen('Wait...') gc.collect() # visible delay caused by this but also sign_it() below self.psbt.sign_it() + + if could_ccc_sign: + dis.fullscreen('CCC Sign...') + gc.collect() + CCCFeature.sign_psbt(self.psbt) + except FraudulentChangeOutput as exc: return await self.failure(exc.args[0], title='Change Fraud') except MemoryError: diff --git a/shared/ccc.py b/shared/ccc.py index 6134658b..92e7da69 100644 --- a/shared/ccc.py +++ b/shared/ccc.py @@ -10,11 +10,9 @@ from menu import MenuSystem, MenuItem from seed import seed_words_to_encoded_secret from stash import SecretStash, len_to_numwords from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC +from exceptions import CCCPolicyViolationError -class PolicyViolationException(Exception): - pass - class CCCFeature: @classmethod def is_enabled(cls): @@ -44,9 +42,7 @@ class CCCFeature: def get_xfp(cls): # just the XFP ccc = settings.get('ccc') - if ccc: - return ccc['c_xfp'] - + return ccc['c_xfp'] if ccc else None @classmethod def init_setup(cls, words): @@ -103,15 +99,79 @@ class CCCFeature: settings.save() @classmethod - def validate_tx(cls, psbt): - policy = cls.get_policy() - magnitude = policy.get("mag", None) + async def meets_policy(cls, psbt): + # Does policy allow signing this? Else raise why + pol = cls.get_policy() + + # mag + magnitude = pol.get("mag", None) outgoing = psbt.total_value_out - psbt.total_change_value if magnitude < 1000: # it is a BTC, convert to sats magnitude = magnitude * 100000000 + if outgoing > magnitude: - raise PolicyViolationException("magnitude") + raise CCCPolicyViolationError("magnitude") + + # vel + + # whitelist + + # web2fa + # - slow, requires UX, and they might not acheive it... + # - wait until about to do signature + if pol.get('web2fa', False): + psbt.warnings.append(('CCC', 'Web 2FA required.')) + return True + + + @classmethod + def could_sign(cls, psbt): + # We are looking at a PSBT: can we sign it, and would we? + # - if we **could** but will not, due to policy, add warning msg + # - return (we could sign, needs2fa step) + if not cls.is_enabled: + return False, False + + ms = psbt.active_multisig + if not ms: + # single-sig CCC not supported + return False, False + + xfp = cls.get_xfp() + if (xfp not in ms.xfp_paths): + # does not involve us + return False, False + + try: + # check policy + needs_2fa = cls.meets_policy(psbt) + except CCCPolicyViolationError: + psbt.warning.append(('CCC', "Violates spending policy. Won't sign.")) + return False, False + + return True, needs_2fa + + @classmethod + async def web2fa_challenge(cls): + # they are trying to sign something, so make them get our their phone + # - at this point they have already ok'ed the details of the txn + # - and we have approved other elements of the spending policy. + # - TODO: maybe show wallet name? or even txn details?? (but info leak to Coinkite) + pol = cls.get_policy() + ss = pol.get('web2fa') + assert ss + + ok = await web2fa.perform_web2fa('Approve CCC Transaction', ss) + if not ok: + raise CCCPolicyViolationError + + @classmethod + def sign_psbt(cls, psbt): + # do the math + # TODO: capture the block height if vel is defined; no going back after this pt. + psbt.sign_it(cls.get_encoded_secret(), cls.get_xfp()) + def render_mag_value(mag): # handle integer bitcoins, and satoshis in same value @@ -544,6 +604,7 @@ 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) diff --git a/shared/exceptions.py b/shared/exceptions.py index 2d92d136..578ba9a3 100644 --- a/shared/exceptions.py +++ b/shared/exceptions.py @@ -51,4 +51,8 @@ class QRDecodeExplained(ValueError): class UnknownAddressExplained(ValueError): pass +# We're not going to co-sign using CCC feature +class CCCPolicyViolationError(RuntimeError): + pass + # EOF