From 9abd2fdbee5b99d117a0196f264679ead544875d Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Sat, 21 Mar 2026 00:44:32 +0700 Subject: [PATCH 1/2] Improved PIN/PUK input UX: added Ctrl+C cancellation support --- cryptnox_cli/command/command.py | 2 +- cryptnox_cli/command/helper/security.py | 58 ++++++++++++++++++++++--- cryptnox_cli/command/helper/ui.py | 7 ++- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/cryptnox_cli/command/command.py b/cryptnox_cli/command/command.py index 8313289..0f2d894 100644 --- a/cryptnox_cli/command/command.py +++ b/cryptnox_cli/command/command.py @@ -98,7 +98,7 @@ def run_execute(self, card) -> int: except (cryptnox_sdk_py.exceptions.CryptnoxException, NotImplementedError) as error: print(error) result = -1 - except ui.ExitException: + except (ui.ExitException, security.ExitException): print("Exited by user.") result = -1 except Unauthorized as error: diff --git a/cryptnox_cli/command/helper/security.py b/cryptnox_cli/command/helper/security.py index d8cf54b..8569e04 100644 --- a/cryptnox_cli/command/helper/security.py +++ b/cryptnox_cli/command/helper/security.py @@ -14,17 +14,57 @@ class ExitException(Exception): """Raised when user has indicated he want's to exit the command""" +def _getpass(prompt='Password: ', mask='*'): + """ + Cross-platform getpass that properly raises KeyboardInterrupt on Ctrl+C. + + On Windows, msvcrt.getch() blocks the Python interpreter so pending + KeyboardInterrupt (from Ctrl+C) is never delivered. We poll with kbhit() + and short sleeps so Python can process signals between polls. + """ + import sys + if sys.platform == 'win32': + import time + from msvcrt import getch, kbhit + entered = [] + sys.stdout.write(prompt) + sys.stdout.flush() + while True: + while not kbhit(): + time.sleep(0.05) # allow Python to raise KeyboardInterrupt + key = ord(getch()) + if key == 3: # Ctrl+C returned as character (non-processed mode) + sys.stdout.write('\n') + sys.stdout.flush() + raise KeyboardInterrupt + elif key == 13: # Enter + sys.stdout.write('\n') + sys.stdout.flush() + return ''.join(entered) + elif key in (8, 127): # Backspace/Del + if entered: + sys.stdout.write('\b \b') + sys.stdout.flush() + entered.pop() + elif 32 <= key <= 126: # Printable ASCII + entered.append(chr(key)) + sys.stdout.write(mask) + sys.stdout.flush() + else: + from stdiomask import getpass + return getpass(prompt, mask) + + def _secret_with_exit(text, required=True): """ Local implementation of secret_with_exit to avoid circular import. Replicates the functionality of ui.secret_with_exit. """ - from stdiomask import getpass - while True: - value = getpass(text).strip() - if value.lower() == "exit": - raise ExitException + try: + value = _getpass(text).strip() + except KeyboardInterrupt: + raise ExitException() if required and not value: print("This entry is required") else: @@ -93,6 +133,9 @@ def check_pin_code(card, text: str = "Cryptnox PIN code: ") -> str: pin_code = "1" easy_mode = is_easy_mode(card.info) + if not easy_mode: + print("Press Ctrl+C to cancel.") + while not authorized: if easy_mode: print(f"Card is in {EASY_MODE_TEXT}. Using easy mode PIN automatically.") @@ -112,6 +155,8 @@ def check_pin_code(card, text: str = "Cryptnox PIN code: ") -> str: prompt_text = text except cryptnox_sdk_py.exceptions.PinBlockedException: raise + except ExitException: + raise except cryptnox_sdk_py.exceptions.PinException: print("Card requires a power cycle. Please remove and re-tap the card, then try again.") raise @@ -159,6 +204,9 @@ def process_command_with_puk( **kwargs) -> bool: easy_mode = is_easy_mode(card.info) + if not easy_mode: + print("Press Ctrl+C to cancel.") + while True: if easy_mode: print(f"Card is in {EASY_MODE_TEXT}. Using easy mode PUK automatically.") diff --git a/cryptnox_cli/command/helper/ui.py b/cryptnox_cli/command/helper/ui.py index 09bc040..2867a71 100644 --- a/cryptnox_cli/command/helper/ui.py +++ b/cryptnox_cli/command/helper/ui.py @@ -10,7 +10,7 @@ from typing import List, Union, Dict import cryptnox_sdk_py -from stdiomask import getpass +from .security import _getpass as getpass from tabulate import tabulate from . import security @@ -60,7 +60,10 @@ def confirm(text: str, warning: str = "") -> bool: def input_with_exit(text, required=True, input_method=None): input_method = input_method or input while True: - value = input_method(text).strip() + try: + value = input_method(text).strip() + except KeyboardInterrupt: + raise ExitException() if value.lower() == "exit": raise ExitException if required and not value: From bab47d7b3a8d5fd97f57c11805e34d094fb0fa42 Mon Sep 17 00:00:00 2001 From: YanNaingWinn Date: Wed, 25 Mar 2026 00:52:19 +0700 Subject: [PATCH 2/2] Simplified Windows getpass: removed kbhit/sleep polling loop --- cryptnox_cli/command/helper/security.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/cryptnox_cli/command/helper/security.py b/cryptnox_cli/command/helper/security.py index 8569e04..9fe50f3 100644 --- a/cryptnox_cli/command/helper/security.py +++ b/cryptnox_cli/command/helper/security.py @@ -16,24 +16,19 @@ class ExitException(Exception): def _getpass(prompt='Password: ', mask='*'): """ - Cross-platform getpass that properly raises KeyboardInterrupt on Ctrl+C. - - On Windows, msvcrt.getch() blocks the Python interpreter so pending - KeyboardInterrupt (from Ctrl+C) is never delivered. We poll with kbhit() - and short sleeps so Python can process signals between polls. + Cross-platform getpass that raises KeyboardInterrupt on Ctrl+C. + On Windows, getch() returns 0x03 for Ctrl+C in raw mode, + sow we catch it directly without polling. """ import sys if sys.platform == 'win32': - import time - from msvcrt import getch, kbhit + from msvcrt import getch entered = [] sys.stdout.write(prompt) sys.stdout.flush() while True: - while not kbhit(): - time.sleep(0.05) # allow Python to raise KeyboardInterrupt key = ord(getch()) - if key == 3: # Ctrl+C returned as character (non-processed mode) + if key == 3: # Ctrl+C sys.stdout.write('\n') sys.stdout.flush() raise KeyboardInterrupt