From 91309281dbd235b4aecd04eda75aa1b48b2106ed Mon Sep 17 00:00:00 2001 From: CJ <99046893+Mystic-Ivy@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:28:28 +0100 Subject: [PATCH 1/2] Added an Opt-In TOTP feature with a more dynamic TOTP creation --- qubes-keepass.ini | 1 + qubes-keepass.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/qubes-keepass.ini b/qubes-keepass.ini index b2c8ee9..e8318b6 100644 --- a/qubes-keepass.ini +++ b/qubes-keepass.ini @@ -5,6 +5,7 @@ smart_sort = True restricted = unrestricted = minimum_trust = 0 +view_totp = True [qubes.trust] trust_level_red = 1 diff --git a/qubes-keepass.py b/qubes-keepass.py index 230147a..f838422 100755 --- a/qubes-keepass.py +++ b/qubes-keepass.py @@ -595,6 +595,35 @@ def get_secret(self) -> str: self.item.load_secret_sync() return self.item.get_secret().get_text() + + def get_totp(self) -> str: + ''' + Obtain the TOTP token from selected entry + + Parameters: + None + + Returns: + TOTP token + + + Current issue with self.item.get_attributes().get('TOTP') does not reload the database. + This results in a non renual of the TOTP attribute. + ''' + + if Config.getboolean('view_totp'): + try: + import pyotp + except ImportError: + print(f'[-] To use TOTP the library pyotp is needed') + return + + if "totp" in self.item.get_attributes().get('otp'): + otp_value = self.item.get_attributes().get('otp').split("?")[1].split("&")[0].split("=") + if otp_value[0] == "secret": + return pyotp.TOTP(otp_value[1]).now() + + def copy_to_qube(self, attribute: int, qube: str, trust_level: int) -> None: ''' Copy the specified attribute to the specified qube. If the credential @@ -654,6 +683,12 @@ def copy_to_qube(self, attribute: int, qube: str, trust_level: int) -> None: print(f'[+] Copying url of credential {self.title} to {qube}.') value = self.url + elif attribute == 13: + if self.totp is None: + return + print(f'[+] Copying url of TOTP {self.title} to {qube}.') + value = self.totp + perform_copy(qube, value) qube_hash = hashlib.md5(qube.encode()).hexdigest() @@ -778,6 +813,9 @@ def __str__(self) -> str: line += lcut(credential.title, Config.getint('title_length')) line += lcut(folder, Config.getint('folder_length')) + if Config.getboolean('view_totp'): + totp_state = 'True' if credential.attributes.get('TOTP') is not None else 'None' + line += lcut(totp_state, Config.getint('totp_length')) line += lcut(credential.username, Config.getint('username_length')) line += lcut(credential.url, Config.getint('url_length')) @@ -812,6 +850,8 @@ def display_rofi(self, qube: str = 'Qube') -> (int, Credential): rofi_mesg = f'Selected credential is copied to {qube}\n\n' rofi_mesg += lcut('Title', title_length) rofi_mesg += lcut('Folder', Config.getint('folder_length')) + if Config.getboolean('view_totp'): + rofi_mesg += lcut('TOTP', Config.getint('totp_length')) rofi_mesg += lcut('Username', Config.getint('username_length')) rofi_mesg += lcut('URL', Config.getint('url_length')) @@ -832,7 +872,6 @@ def display_rofi(self, qube: str = 'Qube') -> (int, Credential): except ValueError: raise RofiAbortedException('rofi selection was aborted by user') - print(f'[+] User selected {self.credentials[selected].title} with return code {process.returncode}') return (process.returncode, self.credentials[selected]) @@ -891,7 +930,7 @@ def main() -> None: print("[-] The configuration options 'restricted' and 'unrestricted' are mutually exclusive.") print('[-] Configure only one of them and leave the other empty to continue.') return - + try: service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION | Secret.ServiceFlags.LOAD_COLLECTIONS) From 66aa493648cd4fabf801b5b396fa5f0764879daa Mon Sep 17 00:00:00 2001 From: CJ <99046893+Mystic-Ivy@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:51:45 +0100 Subject: [PATCH 2/2] Added an Opt-In TOTP feature fixes --- qubes-keepass.ini | 4 +++- qubes-keepass.py | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/qubes-keepass.ini b/qubes-keepass.ini index e8318b6..51b3bec 100644 --- a/qubes-keepass.ini +++ b/qubes-keepass.ini @@ -5,7 +5,7 @@ smart_sort = True restricted = unrestricted = minimum_trust = 0 -view_totp = True +view_totp = False [qubes.trust] trust_level_red = 1 @@ -22,11 +22,13 @@ title_length = 18 folder_length = 18 username_length = 18 url_length = 0 +totp_length = 6 [rofi.shortcuts] copy_url = Ctrl+U copy_password = Ctrl+c copy_username = Ctrl+b +copy_totp = Ctrl+t [rofi.options] rofi_options_1 = -p diff --git a/qubes-keepass.py b/qubes-keepass.py index f838422..9080f82 100755 --- a/qubes-keepass.py +++ b/qubes-keepass.py @@ -684,10 +684,10 @@ def copy_to_qube(self, attribute: int, qube: str, trust_level: int) -> None: value = self.url elif attribute == 13: - if self.totp is None: + if self.attributes.get('TOTP') is None: return print(f'[+] Copying url of TOTP {self.title} to {qube}.') - value = self.totp + value = self.get_totp() perform_copy(qube, value) @@ -796,7 +796,7 @@ def filter_credentials(self, qube: str, trust_level: int) -> None: def __str__(self) -> str: ''' The string representiation of a CredentialCollection is a formatted list - that can be displayed within rogi. + that can be displayed within rofi. Parameters: credentials list of credentials to display @@ -807,7 +807,6 @@ def __str__(self) -> str: formatted = '' for credential in self.credentials: - line = '' folder = credential.path.parent.name or 'Root' @@ -858,6 +857,7 @@ def display_rofi(self, qube: str = 'Qube') -> (int, Credential): mappings = ['-kb-custom-1', Config.get('copy_password')] mappings += ['-kb-custom-2', Config.get('copy_username')] mappings += ['-kb-custom-3', Config.get('copy_url')] + mappings += ['-kb-custom-4', Config.get('copy_totp')] print('[+] Starting rofi.') process = subprocess.Popen(['rofi'] + Config.get_rofi_options() + ['-mesg', rofi_mesg] + mappings, @@ -930,7 +930,7 @@ def main() -> None: print("[-] The configuration options 'restricted' and 'unrestricted' are mutually exclusive.") print('[-] Configure only one of them and leave the other empty to continue.') return - + try: service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION | Secret.ServiceFlags.LOAD_COLLECTIONS)