diff --git a/ykman/cli/openpgp.py b/ykman/cli/openpgp.py index e1f4c36e..6f7f7380 100644 --- a/ykman/cli/openpgp.py +++ b/ykman/cli/openpgp.py @@ -28,7 +28,7 @@ import logging import click from ..util import parse_certificates, parse_private_key -from ..openpgp import OpenPgpController, KEY_SLOT, TOUCH_MODE, get_openpgp_info +from ..openpgp import OpenPgpController, KEY_SLOT, TOUCH_MODE, get_openpgp_info, SEX from .util import ( cli_fail, click_force_option, @@ -414,3 +414,178 @@ def import_certificate(ctx, key, cert, admin_pin): except Exception as e: logger.debug("Failed to import", exc_info=e) cli_fail("Failed to import certificate") + + +@openpgp.group("data") +def data(): + """ + Manage and get data. + """ + + +@data.command("name") +@click.option("-n", "--name", help="New name.") +@click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") +@click.pass_context +def name(ctx, name, admin_pin): + """ + Return and change the saved name. + + Overwrite it when a new name and pin is set. + """ + controller = ctx.obj["controller"] + + if name is not None: + if admin_pin is None: + admin_pin = click_prompt("Enter ADMIN PIN", hide_input=True) + + try: + controller.verify_admin(admin_pin) + controller.set_name(name) + except Exception as e: + logger.debug("Failed to set new name", exc_info=e) + cli_fail("Failed to set new name") + + try: + name = controller.get_name() + except Exception as e: + logger.debug("Failed to get name", exc_info=e) + # cli_fail("Failed to get name") + raise e + + click.echo(f"Name is {name}.") + + +@data.command("login_data") +@click.option("-l", "--login_data", help="New login_data.") +@click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") +@click.pass_context +def login_data(ctx, login_data, admin_pin): + """ + Return and change the saved login data. + + Overwrite it when a new login data and pin is set. + """ + controller = ctx.obj["controller"] + + if login_data is not None: + if admin_pin is None: + admin_pin = click_prompt("Enter ADMIN PIN", hide_input=True) + + try: + controller.verify_admin(admin_pin) + controller.set_login_data(login_data) + except Exception as e: + logger.debug("Failed to set new login data", exc_info=e) + cli_fail("Failed to set new login data") + + try: + login_data = controller.get_login_data() + except Exception as e: + logger.debug("Failed to get login data", exc_info=e) + cli_fail("Failed to get login data") + + click.echo(f"Login data is {login_data}.") + + +@data.command("lang") +@click.option("-l", "--language_pref", help="New language preference.") +@click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") +@click.pass_context +def lang(ctx, language_pref, admin_pin): + """ + Return and change the saved language preference. + + Overwrite it when a new language preference and pin is set. + """ + controller = ctx.obj["controller"] + + if language_pref is not None: + if admin_pin is None: + admin_pin = click_prompt("Enter ADMIN PIN", hide_input=True) + + try: + controller.verify_admin(admin_pin) + controller.set_language_pref(language_pref) + except Exception as e: + logger.debug("Failed to set new language preference", exc_info=e) + cli_fail("Failed to set new language preference") + + try: + language_pref = controller.get_language_pref() + except Exception as e: + logger.debug("Failed to get language preference", exc_info=e) + cli_fail("Failed to get language preference") + + click.echo(f"Language preference is {language_pref}.") + + +@data.command("sex") +@click.option("-s", "--sex", help="New sex.") +@click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") +@click.pass_context +def sex(ctx, sex, admin_pin): + """ + Return and change the saved sex. + + Overwrite it when a new sex and pin is set. + Possible options are not_kown, male, female and not_applicable + """ + controller = ctx.obj["controller"] + + if sex is not None: + try: + sex = SEX.for_name(sex) + except Exception as e: + logger.debug(f"Invalid sex {sex}", exc_info=e) + cli_fail(f"Invalid sex {sex}") + + if admin_pin is None: + admin_pin = click_prompt("Enter ADMIN PIN", hide_input=True) + + try: + controller.verify_admin(admin_pin) + controller.set_sex(sex) + except Exception as e: + logger.debug("Failed to set new sex", exc_info=e) + cli_fail("Failed to set new sex") + + try: + sex = controller.get_sex() + except Exception as e: + logger.debug("Failed to get sex", exc_info=e) + cli_fail("Failed to get sex") + + click.echo(f"Sex is {sex}.") + + +@data.command("url") +@click.option("-u", "--url", help="New url.") +@click.option("-a", "--admin-pin", help="Admin PIN for OpenPGP.") +@click.pass_context +def url(ctx, url, admin_pin): + """ + Return and change the saved url. + + Overwrite it when a new url and pin is set. + """ + controller = ctx.obj["controller"] + + if url is not None: + if admin_pin is None: + admin_pin = click_prompt("Enter ADMIN PIN", hide_input=True) + + try: + controller.verify_admin(admin_pin) + controller.set_url(url) + except Exception as e: + logger.debug("Failed to set new url", exc_info=e) + cli_fail("Failed to set new url") + + try: + url = controller.get_url() + except Exception as e: + logger.debug("Failed to get url", exc_info=e) + cli_fail("Failed to get url") + + click.echo(f"Url is {url}.") diff --git a/ykman/openpgp.py b/ykman/openpgp.py index 58546bc6..bc925a4a 100755 --- a/ykman/openpgp.py +++ b/ykman/openpgp.py @@ -137,6 +137,12 @@ class DO(IntEnum): CARDHOLDER_CERTIFICATE = 0x7F21 ATT_CERTIFICATE = 0xFC KDF = 0xF9 + NAME = 0x5B + LOGIN_DATA = 0x5E + LANGUAGE_PREF = 0x5F2D + SEX = 0x5F35 + URL = 0x5F50 + CARDHOLDER_DATA = 0x65 @unique @@ -159,6 +165,31 @@ def for_name(cls, name): raise ValueError("Unsupported curve: " + name) +@unique +class SEX(IntEnum): + NOT_KNOWN = 0x30 + MALE = 0x31 + FEMALE = 0x32 + NOT_APPLICABLE = 0x39 + + def __str__(self): + if self == SEX.NOT_KNOWN: + return "Not known" + elif self == SEX.MALE: + return "Male" + elif self == SEX.FEMALE: + return "Female" + elif self == SEX.NOT_APPLICABLE: + return "Not applicable" + + @classmethod + def for_name(cls, name): + try: + return getattr(cls, name.upper()) + except AttributeError: + raise ValueError("Unsupported sex: " + name) + + def _get_curve_name(key): if isinstance(key, ec.EllipticCurvePrivateKey): return key.curve.name @@ -590,6 +621,69 @@ def attest(self, key_slot): self._app.send_apdu(0x80, INS.GET_ATTESTATION, key_slot.indx, 0) return self.read_certificate(key_slot) + def set_name(self, name): + """Requires Admin PIN verification.""" + if len(name) > 39: + raise ValueError("Name has to be between 0 and 39 characters.") + + name = name.encode() + + self._put_data(DO.NAME, name) + + def set_login_data(self, login_data): + """Requires Admin PIN verification.""" + login_data = login_data.encode() + + self._put_data(DO.LOGIN_DATA, login_data) + + def set_language_pref(self, language_pref): + """Requires Admin PIN verification.""" + if len(language_pref) < 2 or len(language_pref) > 8: + raise ValueError( + "Language preference has to be between 2 and 8 characters." + ) + + language_pref = language_pref.encode() + + self._put_data(DO.LANGUAGE_PREF, language_pref) + + def set_sex(self, sex): + """Requires Admin PIN verification.""" + sex = struct.pack(">B", sex) + + self._put_data(DO.SEX, sex) + + def set_url(self, url): + """Requires Admin PIN verification.""" + url = url.encode() + + self._put_data(DO.URL, url) + + def get_name(self): + data = self._get_data(DO.CARDHOLDER_DATA) + len = data[3] + name = data[4 : len + 4] + + return name.decode() + + def get_login_data(self): + return self._get_data(DO.LOGIN_DATA).decode() + + def get_language_pref(self): + data = self._get_data(DO.CARDHOLDER_DATA) + name_len = data[3] + len = data[name_len + 6] + lang = data[name_len + len + 9 : name_len + len + 9] + + return lang.decode() + + def get_sex(self): + sex = self._get_data(DO.CARDHOLDER_DATA)[-1] + return SEX(sex) + + def get_url(self): + return self._get_data(DO.URL).decode() + def get_openpgp_info(controller: OpenPgpController) -> str: """Get human readable information about the OpenPGP configuration."""