Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage openpgp data #477

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 176 additions & 1 deletion ykman/cli/openpgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}.")
94 changes: 94 additions & 0 deletions ykman/openpgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down