def show(self): local_critical( *self.messages, support_hint=self.support_hint, ret_code=self.ret_code, **self.kwargs, )
def update(regnual, gnuk, default_password, password, wait_e, keyno, verbose, yes, skip_bootloader, green_led): """update device's firmware""" args = (regnual, gnuk, default_password, password, wait_e, keyno, verbose, yes, skip_bootloader, green_led) if green_led and (regnual is None or gnuk is None): local_critical( "You selected the --green-led option, please provide '--regnual' and " "'--gnuk' in addition to proceed. ", "use one from: https://github.com/Nitrokey/nitrokey-start-firmware)") if IS_LINUX: with ThreadLog(logger.getChild("dmesg"), "dmesg -w"): start_update(*args) else: start_update(*args)
def get_latest_release_data(): try: # @todo: move to confconsts.py r = requests.get( 'https://api.github.com/repos/Nitrokey/nitrokey-start-firmware/releases' ) json = r.json() if r.status_code == 403: local_critical( f"JSON raw data: {json}", f"No Github API access, status code: {r.status_code}") latest_tag = json[0] except Exception as e: local_critical("Failed getting release data", e) latest_tag = defaultdict(lambda: "unknown") return latest_tag
def set_identity(identity): """set given identity (one of: 0, 1, 2)""" if not identity.isdigit(): local_critical("identity number must be a digit") identity = int(identity) if identity < 0 or identity > 2: local_print("identity must be 0, 1 or 2") local_print(f"Setting identity to {identity}") for x in range(3): try: gnuk = get_gnuk_device() gnuk.cmd_select_openpgp() try: gnuk.cmd_set_identity(identity) except USBError: local_print(f"reset done - now active identity: {identity}") break except ValueError as e: if "No ICC present" in str(e): local_print("Could not connect to device, trying to close scdaemon") result = check_output(["gpg-connect-agent", "SCD KILLSCD", "SCD BYE", "/bye"]) # gpgconf --kill all might be better? sleep(3) else: local_critical(e) except Exception as e: local_critical(e)
def feedkernel(count, serial): """Feed random bytes to /dev/random.""" if os.name != "posix": local_critical("This is a Linux-specific command!") if not 0 <= count <= 255: local_critical( f"Number of bytes must be between 0 and 255, you passed {count}") p = nkfido2.find(serial) RNDADDENTROPY = 0x40085203 entropy_info_file = "/proc/sys/kernel/random/entropy_avail" print(f"entropy before: 0x{open(entropy_info_file).read().strip()}") r = p.get_rng(count) # man 4 random # RNDADDENTROPY # Add some additional entropy to the input pool, incrementing the # entropy count. This differs from writing to /dev/random or # /dev/urandom, which only adds some data but does not increment the # entropy count. The following structure is used: # struct rand_pool_info { # int entropy_count; # int buf_size; # __u32 buf[0]; # }; # Here entropy_count is the value added to (or subtracted from) the # entropy count, and buf is the buffer of size buf_size which gets # added to the entropy pool. # maximum 8, tend to be pessimistic entropy_bits_per_byte = 2 t = struct.pack(f"ii{count}s", count * entropy_bits_per_byte, count, r) try: with open("/dev/random", mode="wb") as fh: fcntl.ioctl(fh, RNDADDENTROPY, t) except PermissionError as e: local_critical( "insufficient permissions to use `fnctl.ioctl` on '/dev/random'", "please run 'nitropy' with proper permissions", e, ) local_print(f"entropy after: 0x{open(entropy_info_file).read().strip()}")
def start_update( regnual, gnuk, default_password, password, wait_e, keyno, verbose, yes, skip_bootloader, green_led, ): # @todo: move to some more generic position... local_print("Nitrokey Start firmware update tool") # @fixme: especially this, which is to be handle application wide logger.debug("Start session {}".format(datetime.now())) local_print("Platform: {}".format(platform.platform())) local_print("System: {}, is_linux: {}".format(platform.system(), IS_LINUX)) local_print("Python: {}".format(platform.python_version())) local_print("Saving run log to: {}".format(LOG_FN)) arg_descs = [ "regnual", "gnuk", "default_password", "password", "wait_e", "keyno", "verbose", "yes", "skip_bootloader", "green_led", ] args = ( regnual, gnuk, default_password, "<hidden>", wait_e, keyno, verbose, yes, skip_bootloader, green_led, ) logger.debug("Arguments: " + ", ".join(f"{key}= '{val}'" for key, val in zip(arg_descs, args))) passwd = None if verbose == 3: stream_handler = logging.StreamHandler() stream_handler.setLevel(logging.DEBUG) stream_handler.setFormatter(logging.Formatter(LOG_FORMAT_STDOUT)) logger.addHandler(stream_handler) if password: passwd = password elif default_password: passwd = DEFAULT_PW3 if not passwd: try: passwd = AskUser.hidden("Admin password:"******"aborting update", e) local_print("Firmware data to be used:") data = get_firmware_file(regnual, FirmwareType.REGNUAL) data_upgrade = get_firmware_file(gnuk, FirmwareType.GNUK) # Detect devices dev_strings = get_devices() if len(dev_strings) > 1: local_critical( "Only one device should be connected", "Please remove other devices and retry", ) if dev_strings: local_print("Currently connected device strings:") print_device(dev_strings[0]) else: local_print("Cannot identify device") # @todo: debugging information, log-file only local_print(f"initial device strings: {dev_strings}") latest_tag = get_latest_release_data() local_print( f"Please note:", f"- Latest firmware available is: ", f" {latest_tag['tag_name']} (published: {latest_tag['published_at']})", f"- provided firmware: {gnuk}", f"- all data will be removed from the device!", f"- do not interrupt update process - the device may not run properly!", f"- the process should not take more than 1 minute", ) if yes: local_print("Accepted automatically") else: if not AskUser.strict_yes_no("Do you want to continue?"): local_critical("Exiting due to user request", support_hint=False) update_done = False retries = 3 for attempt_counter in range(retries): try: # First 4096-byte in data_upgrade is SYS, so, skip it. main( wait_e, keyno, passwd, data, data_upgrade[4096:], skip_bootloader, verbosity=verbose, ) update_done = True break # @todo: add proper exceptions (for each case) here except ValueError as e: local_print("error while running update", e) str_factory_reset = ( "Please 'factory-reset' your device to " "continue (this will delete all user data from the device) " "and try again with PIN='12345678'") if "No ICC present" in str(e): kill_smartcard_services() local_print("retrying...") else: # @fixme run factory reset here since data are lost anyway (rly?) if str(e) == ERR_EMPTY_COUNTER: local_critical( "- device returns: 'Attempt counter empty' " "- error for Admin PIN", str_factory_reset, e, ) if str(e) == ERR_INVALID_PIN: local_critical( "- device returns: 'Invalid PIN' error", "- please retry with correct PIN", e, ) except Exception as e: local_critical("unexpected error", e) if not update_done: local_critical( "", "Could not proceed with the update", "Please execute one or all of the following and try again:", "- re-insert device to the USB slot", "- run factory-reset on the device", "- close other applications, which could use it (e.g., scdaemon, pcscd)", ) dev_strings_upgraded = None takes_long_time = False local_print("Currently connected device strings (after upgrade):") for i in range(TIME_DETECT_DEVICE_AFTER_UPDATE_S): if i > TIME_DETECT_DEVICE_AFTER_UPDATE_LONG_S: if not takes_long_time: local_print("", "Please reinsert device to the USB slot") takes_long_time = True time.sleep(1) dev_strings_upgraded = get_devices() if len(dev_strings_upgraded) > 0: local_print() print_device(dev_strings_upgraded[0]) break local_print(".", end="", flush=True) if not dev_strings_upgraded: local_print( "", "could not connect to the device - might be due to a failed update", "please re-insert the device, check version using:", "$ nitropy start list", ) local_print( f"device can now be safely removed from the USB slot", f"final device strings: {dev_strings_upgraded}", ) # @todo: add this to all logs and skip it here local_print(f"finishing session {datetime.now()}") # @todo: always output this in certain situations... (which ones? errors? warnings?) local_print(f"Log saved to: {LOG_FN}")
def download_file_or_exit(url): resp = requests.get(url) if not resp.ok: local_critical(f"Cannot download firmware: {url}: {resp.status_code}") firmware_data = resp.content return firmware_data
def update(serial, yes): """Update Nitrokey key to latest firmware version.""" # @fixme: print this and allow user to cancel (if not -y is active) #update_url = 'https://update.nitrokey.com/' #print('Please use {} to run the firmware update'.format(update_url)) #return IS_LINUX = platform.system() == "Linux" logger.debug(f"Start session {datetime.now()}") # @fixme: move to generic startup stuff logged into file exclusively! local_print("Nitrokey FIDO2 firmware update tool", f"Platform: {platform.platform()}", f"System: {platform.system()}, is_linux: {IS_LINUX}", f"Python: {platform.python_version()}", f"Saving run log to: {LOG_FN}", "", f"Starting update procedure for Nitrokey FIDO2...") from pynitrokey.fido2 import find # Determine target key client = None try: client = find(serial) except pynitrokey.exceptions.NoSoloFoundError as e: local_critical(None, "No Nitrokey key found!", e, None, "If you are on Linux, are your udev rules up to date?", "For more, see: ", " https://www.nitrokey.com/documentation/installation#os:linux", None) except pynitrokey.exceptions.NonUniqueDeviceError as e: local_critical(None, "Multiple Nitrokey keys are plugged in!", e, None, "Please unplug all but one key", None) except Exception as e: local_critical(None, "Unhandled error connecting to key", e, None) # determine asset url: we want the (signed) json file # @fixme: move to confconsts.py ... api_base_url = "https://api.github.com/repos" api_url = f"{api_base_url}/Nitrokey/nitrokey-fido2-firmware/releases/latest" try: gh_release_data = json.loads(requests.get(api_url).text) except Exception as e: local_critical("Failed downloading firmware", e) # search asset with `fn` suffix being .json and take its url assets = [(x["name"], x["browser_download_url"]) \ for x in gh_release_data["assets"]] download_url = None for fn, url in assets: if fn.endswith(".json"): download_url = url break if not download_url: local_critical("Failed to determine latest release (url)", "assets:", *map(str, assets)) # download asset url # @fixme: move to confconsts.py ... local_print(f"Downloading latest firmware: {gh_release_data['tag_name']} " f"(published at {gh_release_data['published_at']})") tmp_dir = tempfile.gettempdir() fw_fn = os.path.join(tmp_dir, "fido2_firmware.json") try: with open(fw_fn, "wb") as fd: firmware = requests.get(download_url) fd.write(firmware.content) except Exception as e: local_critical("Failed downloading firmware", e) local_print(f"Firmware saved to {fw_fn}", f"Downloaded firmware version: {gh_release_data['tag_name']}") # @fixme: whyyyyy is this here, move away... (maybe directly next to `fido2.find()`) def get_dev_details(): # @fixme: why not use `find` here... from pynitrokey.fido2 import find_all c = find_all()[0] _props = c.dev.descriptor local_print(f"Device connected:") if "serial_number" in _props: local_print(f"{_props['serial_number']}: {_props['product_string']}") else: local_print(f"{_props['path']}: {_props['product_string']}") version_raw = c.solo_version() major, minor, patch = version_raw[:3] locked = "" if len(version_raw) > 3 and version_raw[3] else "unlocked" local_print(f"Firmware version: {major}.{minor}.{patch} {locked}", None) get_dev_details() # ask for permission if not yes: local_print("This will update your Nitrokey FIDO2") if not AskUser.strict_yes_no("Do you want to continue?"): local_critical("exiting due to user input...", support_hint=False) # Ensure we are in bootloader mode if client.is_solo_bootloader(): local_print("Key already in bootloader mode, continuing...") else: try: local_print("Entering bootloader mode, please confirm with button on key!") client.enter_bootloader_or_die() time.sleep(0.5) except Exception as e: local_critical("problem switching to bootloader mode:", e) # reconnect and actually flash it... try: from pynitrokey.fido2 import find client = find(serial) client.use_hid() client.program_file(fw_fn) except Exception as e: local_critical("problem flashing firmware:", e) local_print(None, "After update check") tries = 100 for i in range(tries): try: get_dev_details() break except Exception as e: if i > tries-1: local_critical("Could not connect to device after update", e) raise time.sleep(0.5) local_print("Congratulations, your key was updated to the latest firmware.") logger.debug("Finishing session {}".format(datetime.now())) local_print("Log saved to: {}".format(LOG_FN))