diff --git a/nitrokeyapp/secrets_tab/__init__.py b/nitrokeyapp/secrets_tab/__init__.py index 9ddf893a..fa0a75c6 100644 --- a/nitrokeyapp/secrets_tab/__init__.py +++ b/nitrokeyapp/secrets_tab/__init__.py @@ -1,8 +1,9 @@ import binascii import logging -from base64 import b32decode +from base64 import b32decode, b32encode from datetime import datetime from enum import Enum +from random import randbytes from typing import Callable, Optional from PySide6.QtCore import Qt, QThread, QTimer, Signal, Slot @@ -14,7 +15,7 @@ from nitrokeyapp.qt_utils_mix_in import QtUtilsMixIn from nitrokeyapp.worker import Worker -from .data import Credential, OtpData, OtpKind +from .data import Credential, OtherKind, OtpData, OtpKind from .worker import SecretsWorker # TODO: @@ -124,6 +125,7 @@ def __init__(self, info_box: InfoBox, parent: Optional[QWidget] = None) -> None: icon_refresh = self.get_qicon("OTP_generate.svg") icon_edit = self.get_qicon("edit.svg") icon_visibility = self.get_qicon("visibility_off.svg") + icon_generate = self.get_qicon("refresh.svg") loc = QLineEdit.ActionPosition.TrailingPosition self.action_username_copy = self.ui.username.addAction(icon_copy, loc) @@ -155,6 +157,9 @@ def __init__(self, info_box: InfoBox, parent: Optional[QWidget] = None) -> None: self.action_otp_edit = self.ui.otp.addAction(icon_edit, loc) self.action_otp_edit.triggered.connect(self.act_enable_otp_edit) + self.action_hmac_gen = self.ui.otp.addAction(icon_generate, loc) + self.action_hmac_gen.triggered.connect(self.generate_hmac) + self.line_actions = [ self.action_username_copy, self.action_password_copy, @@ -163,6 +168,7 @@ def __init__(self, info_box: InfoBox, parent: Optional[QWidget] = None) -> None: self.action_otp_copy, self.action_otp_edit, self.action_otp_gen, + self.action_hmac_gen, ] self.line2copy_action = { self.ui.username: self.action_username_copy, @@ -394,6 +400,8 @@ def show_credential(self, credential: Credential) -> None: self.ui.is_pin_protected.setChecked(credential.protected) self.ui.is_touch_protected.setChecked(credential.touch_required) + self.hide_hmac_view() + self.hide_otp() self.ui.algorithm_tab.setCurrentIndex(1) @@ -401,6 +409,7 @@ def show_credential(self, credential: Credential) -> None: self.ui.algorithm_tab.show() self.ui.algorithm_edit.hide() self.ui.algorithm_show.show() + self.action_hmac_gen.setVisible(False) self.ui.otp.show() self.ui.otp.setReadOnly(True) @@ -414,6 +423,12 @@ def show_credential(self, credential: Credential) -> None: self.action_otp_copy.setVisible(False) self.action_otp_gen.setVisible(False) + algo = str(credential.other) + if algo == "HMAC": + self.show_hmac_view() + else: + self.hide_hmac_view() + self.action_otp_edit.setVisible(False) else: self.ui.algorithm_tab.hide() @@ -458,7 +473,6 @@ def edit_credential(self, credential: Credential) -> None: self.ui.comment.setText(credential.comment.decode(errors="replace")) else: self.ui.comment.setText("") - self.ui.name.setReadOnly(False) self.ui.username.setReadOnly(False) self.ui.password.setReadOnly(False) @@ -487,6 +501,7 @@ def edit_credential(self, credential: Credential) -> None: str(credential.otp or credential.other) ) self.ui.select_algorithm.setEnabled(False) + self.action_hmac_gen.setVisible(False) self.action_otp_copy.setVisible(False) self.action_otp_gen.setVisible(False) @@ -510,11 +525,18 @@ def edit_credential(self, credential: Credential) -> None: self.check_credential() + if credential.other == OtherKind.HMAC: + self.show_hmac_view() + self.ui.btn_save.setEnabled(False) + else: + self.hide_hmac_view() + def act_enable_otp_edit(self) -> None: assert self.active_credential self.active_credential.new_secret = True self.ui.otp.setReadOnly(False) + self.ui.select_algorithm.setMaxCount(3) self.ui.select_algorithm.setEnabled(True) self.ui.otp.setPlaceholderText("") self.ui.otp.setText("") @@ -523,6 +545,7 @@ def act_enable_otp_edit(self) -> None: @Slot() def add_new_credential(self) -> None: + if not self.data: return @@ -563,6 +586,8 @@ def add_new_credential(self) -> None: self.ui.otp.setReadOnly(False) self.ui.algorithm_tab.show() + self.ui.select_algorithm.setMaxCount(4) + self.ui.select_algorithm.addItem("HMAC") self.ui.algorithm_tab.setCurrentIndex(0) self.ui.algorithm_edit.show() self.ui.algorithm_show.hide() @@ -570,6 +595,7 @@ def add_new_credential(self) -> None: self.ui.select_algorithm.show() self.ui.select_algorithm.setCurrentText("None") self.ui.select_algorithm.setEnabled(True) + self.action_hmac_gen.setVisible(False) self.ui.btn_abort.show() self.ui.btn_delete.hide() @@ -586,6 +612,13 @@ def check_credential(self) -> None: algo = self.ui.select_algorithm.currentText() if self.ui.select_algorithm.isEnabled(): + if algo == "HMAC": + self.show_hmac_view() + if len(otp_secret) != 32: + can_save = False + else: + self.hide_hmac_view() + if algo != "None" and not is_base32(otp_secret): can_save = False @@ -631,6 +664,54 @@ def hide_credential(self) -> None: self.ui.secrets_list.clearSelection() self.active_credential = None + def show_hmac_view(self) -> None: + + name_hmac = "HmacSlot2" + + if self.active_credential is None: + self.action_otp_copy.setVisible(True) + self.action_hmac_gen.setVisible(True) + self.ui.name_label.setText(name_hmac) + self.ui.name.setText(name_hmac) + else: + self.action_hmac_gen.setVisible(False) + self.action_otp_copy.setVisible(False) + + self.ui.name.hide() + self.ui.name_label.show() + + self.ui.username_label.hide() + self.ui.username.hide() + + self.ui.password_label.hide() + self.ui.password.hide() + + self.ui.comment_label.hide() + self.ui.comment.hide() + + self.ui.is_pin_protection_label.hide() + self.ui.is_pin_protected.hide() + + self.ui.is_touch_protection_label.hide() + self.ui.is_touch_protected.hide() + + def hide_hmac_view(self) -> None: + + self.ui.username_label.show() + self.ui.username.show() + + self.ui.password_label.show() + self.ui.password.show() + + self.ui.comment_label.show() + self.ui.comment.show() + + self.ui.is_pin_protection_label.show() + self.ui.is_pin_protected.show() + + self.ui.is_touch_protection_label.show() + self.ui.is_touch_protected.show() + @Slot() def hide_otp(self) -> None: self.otp_timeout = None @@ -694,11 +775,14 @@ def save_credential(self) -> None: print("INSERT ERROR MESSAGE HERE - status bar?") return - kind, otp_secret, secret = None, None, None + kind, otherKind, otp_secret, secret = None, None, None, None # only save otp, if enabled if self.ui.select_algorithm.isEnabled(): try: - kind = OtpKind.from_str(kind_str) + if kind_str == "HMAC" or kind_str == "REVERSE_HOTP": + otherKind = OtherKind.from_str(kind_str) + else: + kind = OtpKind.from_str(kind_str) otp_secret = self.ui.otp.text() secret = parse_base32(otp_secret) except RuntimeError: @@ -707,6 +791,7 @@ def save_credential(self) -> None: cred = Credential( id=name.encode(), otp=kind, + other=otherKind, login=username.encode(), password=password.encode(), comment=comment.encode(), @@ -734,3 +819,8 @@ def generate_otp(self) -> None: def uncheck_checkbox(self, uncheck: bool) -> None: if uncheck: self.ui.is_protected.setChecked(False) + + @Slot() + def generate_hmac(self) -> None: + secret = b32encode(randbytes(20)) + self.ui.otp.setText(secret.decode()) diff --git a/nitrokeyapp/secrets_tab/data.py b/nitrokeyapp/secrets_tab/data.py index f439222e..d441ed88 100644 --- a/nitrokeyapp/secrets_tab/data.py +++ b/nitrokeyapp/secrets_tab/data.py @@ -41,6 +41,21 @@ class OtherKind(Enum): def __str__(self) -> str: return self.name + def raw_kind(self) -> RawKind: + if self == OtherKind.HMAC: + return RawKind.Hmac + elif self == OtherKind.REVERSE_HOTP: + return RawKind.HotpReverse + else: + raise RuntimeError(f"Unexpected OTP kind: {self}") + + @classmethod + def from_str(cls, s: str) -> "OtherKind": + for kind in OtherKind: + if kind.name == s: + return kind + raise RuntimeError(f"Unexpected Other kind: {kind}") + Kind = Union[OtpKind, OtherKind] diff --git a/nitrokeyapp/secrets_tab/worker.py b/nitrokeyapp/secrets_tab/worker.py index f7952804..a49e01ef 100644 --- a/nitrokeyapp/secrets_tab/worker.py +++ b/nitrokeyapp/secrets_tab/worker.py @@ -396,6 +396,10 @@ def add_credential(self, successful: bool = True) -> None: pin_based_encryption=self.credential.protected, ) + if self.credential.other: + reg_data["secret"] = self.secret + reg_data["kind"] = self.credential.other.raw_kind() + if self.credential.otp: reg_data["secret"] = self.secret reg_data["kind"] = self.credential.otp.raw_kind() @@ -406,7 +410,6 @@ def add_credential(self, successful: bool = True) -> None: reg_data["password"] = self.credential.password if self.credential.comment: reg_data["metadata"] = self.credential.comment - try: secrets.register(**reg_data) # type: ignore [arg-type] except SecretsAppException as e: diff --git a/nitrokeyapp/ui/secrets_tab.ui b/nitrokeyapp/ui/secrets_tab.ui index 63adfcaf..ac94581a 100644 --- a/nitrokeyapp/ui/secrets_tab.ui +++ b/nitrokeyapp/ui/secrets_tab.ui @@ -247,7 +247,7 @@ - + Username: @@ -279,7 +279,7 @@ - + Password: @@ -311,7 +311,7 @@ - + Qt::LeftToRight @@ -339,7 +339,7 @@ - 1 + 0 @@ -410,6 +410,11 @@ HOTP + + + HMAC + + @@ -478,7 +483,7 @@ - + Qt::LeftToRight @@ -507,7 +512,7 @@ - + Qt::RightToLeft