Repository: firecat53/keepmenu (language: python)

Issue:
Implement database-specific autotype defaults
For my use case I use separate databases for passwords and TOTPs security reasons as [recommended](https://keepassxc.org/docs/#faq-security-totp) by KeepassXC. However, in keepmenu's current state, I cannot add autotype defaults to set the appropriate behaviour. 

In the meantime, I've been using this configuration option
```ini
autotype_default = {PASSWORD}{TOTP}{ENTER}
```
and modified [this](https://github.com/firecat53/keepmenu/blob/e301b32be79c69096b7824476f8be00845e32233/keepmenu#L1095) return statement so it doesn't crash when it cannot find the autotype token.
```diff
     except KeyError:
         steam = False
 
-    return totp(
-        query_string['secret'][0], int(query_string['period'][0]),
-        int(query_string['digits'][0]), 'sha1' if 'algorihm'
-        not in query_string else query_string['algorithm'][0].lower(), steam)
+    try:
+        return totp(
+            query_string['secret'][0], int(query_string['period'][0]),
+            int(query_string['digits'][0]), 'sha1' if 'algorihm'
+            not in query_string else query_string['algorithm'][0].lower(), steam)
+    except KeyError:
+        return
 
 
 def add_entry(kpo):
```

but this prevents me from using something like this
```ini
autotype_default_1= {USERNAME}{TAB}{PASSWORD}{ENTER}
autotype_default_2 = {TOTP}{ENTER}


Discussion / hints:
I am willing to implement this, looked into it and doesn't look too difficult to add. @firecat53 does the way @dylantjb recommended setting this up in the config.ini look good? 
How about instead of complicating config.ini we just add command line flags for
database, keyfile and/or config.ini. That seems a cleaner solution (also fixes
#91 and might allow for some easier single run troubleshooting without starting
the server). What do you all think?

Why don't we add both? We can keep `autotype_default` and `autotype_default_1`, `autotype_default_2`, etc will override `autotype_default` when they are set so if someone isn't going to use specific autotypes for different databases then they don't need to complicate their config.ini.
Hmm, I guess that makes some sense. Ok, we'll try implementing both and see what the code ends up looking like.

Fix patch:
diff --git a/README.rst b/README.rst
index ee0d10e..46266a7 100644
--- a/README.rst
+++ b/README.rst
@@ -105,7 +105,7 @@ Installation
 
     * When using xdotool, call `setxkbmap` to set your keyboard type somewhere
       in your window manager or desktop environment initialization. For example:
-      `exec setxkbmap de` in ~/.config/i3/config. 
+      `exec setxkbmap de` in ~/.config/i3/config.
 
 - If using Rofi, pass desired theme via `dmenu_command = rofi -theme
   <theme>.rasi`. Dmenu options are also passed via `dmenu_command`
@@ -136,6 +136,14 @@ Usage
   select the entry, select 'Password' then select 'Manually enter password'.
   Type 'ESC' to exit without making changes.
 
+Options
+-------
+keepmenu [-h] [-a AUTOTYPE]
+
+--help, -h Output a usage message and exit.
+
+-a AUTOTYPE, --autotype AUTOTYPE Override autotype sequence in config.ini
+
 Tests
 -----
 
@@ -143,7 +151,7 @@ Tests
 
 .. _Rofi: https://davedavenport.github.io/rofi/
 .. _Passhole: https://github.com/purduelug/passhole
-.. _Keepass field references: https://keepass.info/help/base/fieldrefs.html 
+.. _Keepass field references: https://keepass.info/help/base/fieldrefs.html
 .. _Pykeepass: https://github.com/pschmitt/pykeepass
 .. _pynput: https://github.com/moses-palmer/pynput
 .. _Archlinux AUR: https://aur.archlinux.org/packages/python-keepmenu-git
diff --git a/config.ini.example b/config.ini.example
index 9778e85..af7a6bb 100644
--- a/config.ini.example
+++ b/config.ini.example
@@ -17,6 +17,8 @@
 # password_1 = database password  **INSECURE**
 # password_cmd_2 = <command to generate db password>
 # database_2 = <path/to/second database>
+# # Override autotype default from database_2
+# autotype_default_2 = {TOTP}{ENTER}
 # etc....
 # pw_cache_period_min = <minutes to cache database password>
 
diff --git a/keepmenu b/keepmenu
index d8a254f..d6e7d6f 100755
--- a/keepmenu
+++ b/keepmenu
@@ -3,6 +3,7 @@
 """Read and copy Keepass database entries using dmenu or rofi
 
 """
+import argparse
 import base64
 import configparser
 from contextlib import closing
@@ -288,9 +289,9 @@ def get_password_chars():
 def get_database(open_databases=[]):
     """Read databases from config or ask for user input.
 
-    Args: open_databases - list [(db_name, keyfile, pw), (db_name2, kf, pw),...]
-    Returns: (database name, keyfile, passphrase)
-             Returns (None, None, None) on error selecting database or password
+    Args: open_databases - list [(db_name, keyfile, pw, autotype), (db_name2, kf, pw, autotype),...]
+    Returns: (database name, keyfile, passphrase, autotype)
+             Returns (None, None, None, None) on error selecting database or password
 
     """
     args = CONF.items('database')
@@ -308,6 +309,10 @@ def get_database(open_databases=[]):
             passw = args_dict['password_{}'.format(idx)]
         except KeyError:
             passw = ''
+        try:
+            autotype = args_dict['autotype_default_{}'.format(idx)]
+        except KeyError:
+            autotype = None
         try:
             cmd = args_dict['password_cmd_{}'.format(idx)]
             res = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE).communicate()
@@ -319,28 +324,28 @@ def get_database(open_databases=[]):
         except KeyError:
             pass
         if dbn:
-            dbs.append((dbn, keyfile, passw))
+            dbs.append((dbn, keyfile, passw, autotype))
     if not dbs:
         res = get_initial_db()
         if res is True:
             dbs = [get_database()]
         else:
-            return (None, None, None)
+            return (None, None, None, None)
     if len(dbs) > 1:
         inp_bytes = "\n".join(i[0] for i in dbs).encode(ENC)
         sel = dmenu_select(len(dbs), "Select Database", inp=inp_bytes)
         dbs = [i for i in dbs if i[0] == sel]
         if not sel or not dbs:
-            return (None, None, None)
+            return (None, None, None, None)
     for dbr in open_databases:
         # Match db name to open_databases
         if dbr[0] == dbs[0][0]:
             return dbr
-    if not dbs[0][-1]:
+    if not dbs[0][-2]:
         db_l = list(dbs[0])
-        db_l[-1] = get_passphrase()
-        if not db_l[-1]:
-            return (None, None, None)
+        db_l[-2] = get_passphrase()
+        if not db_l[-2]:
+            return (None, None, None, None)
         dbs[0] = db_l
     return tuple(dbs[0])
 
@@ -371,7 +376,7 @@ def get_entries(dbo):
 
     """
     from pykeepass import PyKeePass
-    dbf, keyfile, password = dbo
+    dbf, keyfile, password, _ = dbo
     if dbf is None:
         return None
     try:
@@ -482,20 +487,28 @@ def token_command(token):
     return None
 
 
-def type_entry(entry):
+def type_entry(entry, db_autotype=None):
     """Pick which library to use to type strings
 
     Defaults to pynput
 
+    Args: entry - The entry to type
+          db_autotype - the database specific autotype that overrides 'autotype_default'
+
     """
     sequence = SEQUENCE
+
     if hasattr(entry, 'autotype_enabled') and entry.autotype_enabled is False:
         dmenu_err("Autotype disabled for this entry")
         return
+    if db_autotype is not None and db_autotype != '':
+        sequence = db_autotype
     if hasattr(entry, 'autotype_sequence') and \
             entry.autotype_sequence is not None and \
             entry.autotype_sequence != 'None':
         sequence = entry.autotype_sequence
+    if ARGS.autotype is not None:
+        sequence = ARGS.autotype
     tokens = tokenize_autotype(sequence)
 
     library = 'pynput'
@@ -1087,6 +1100,10 @@ def gen_otp(otp_url):
     parsed_otp_url = parse.urlparse(otp_url)
     query_string = parse.parse_qs(parsed_otp_url.query)
 
+    for required in ('secret', 'period', 'digits'):
+        if required not in query_string:
+            return ''
+
     try:
         steam = query_string['encoder'][0] == "steam"
     except KeyError:
@@ -1545,7 +1562,7 @@ class DmenuRunner(Process):
                 entry = filtered_entries[int(sel.split('-', 1)[0])]
             except (ValueError, TypeError):
                 return
-            type_entry(entry)
+            type_entry(entry, self.database[3])
             self.prev_entry = entry
 
     def menu_view_type_individual_entries(self, hid_groups):
@@ -1692,9 +1709,21 @@ def run():
 
 
 if __name__ == '__main__':
-    if len(sys.argv) > 1:
-        print("See `man keepmenu` for help")
-        sys.exit()
+    global ARGS
+
+    parser = argparse.ArgumentParser(
+        description="Dmenu/Rofi frontend for Keepass databases")
+
+    parser.add_argument(
+        "-a",
+        "--autotype",
+        type=str,
+        required=False,
+        help="Override autotype sequence in config.ini",
+    )
+
+    ARGS = parser.parse_args()
+
     try:
         MANAGER = client()
         MANAGER.set_event()  # pylint: disable=no-member
diff --git a/keepmenu.1 b/keepmenu.1
index a13c699..c5fb14f 100644
--- a/keepmenu.1
+++ b/keepmenu.1
@@ -174,5 +174,15 @@ typed.
 option, then select the entry, select \fI"Password"\fP then select \fI"Manually
 enter password"\fP. Type "ESC" to exit without making changes.
 
+
+.SH Options
+.PP
+keepmenu [-h] [-a AUTOTYPE]
+.PP
+--help, -h Output a usage message and exit.
+.PP
+-a AUTOTYPE, --autotype AUTOTYPE Override autotype sequence in
+config.ini
+
 .SH TESTS
 \fB1.\fR To run tests: \fIpython tests/tests.py\fP


Test patch:
diff --git a/tests/tests.py b/tests/tests.py
index 8390795..efbe41c 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -177,7 +177,7 @@ def test_open_database(self):
             KM.CONF.set('database', 'database_1', db_name)
             KM.CONF.write(conf_file)
         database = KM.get_database()
-        self.assertTrue(database == (db_name, '', 'password'))
+        self.assertTrue(database == (db_name, '', 'password', None))
         kpo = KM.get_entries(database)
         self.assertIsInstance(kpo, PyKeePass)
         # Switch from `password_1` to `password_cmd_1`
@@ -186,9 +186,17 @@ def test_open_database(self):
             KM.CONF.set('database', 'password_cmd_1', 'echo password')
             KM.CONF.write(conf_file)
         database = KM.get_database()
-        self.assertTrue(database == (db_name, '', 'password'))
+        self.assertTrue(database == (db_name, '', 'password', None))
         kpo = KM.get_entries(database)
         self.assertIsInstance(kpo, PyKeePass)
+        with open(KM.CONF_FILE, 'w') as conf_file:
+            KM.CONF.set('database', 'autotype_default_1', '{TOTP}{ENTER}')
+            KM.CONF.write(conf_file)
+        database = KM.get_database()
+        self.assertTrue(database == (db_name, '', 'password', '{TOTP}{ENTER}'))
+        kpo = KM.get_entries(database)
+        self.assertIsInstance(kpo, PyKeePass)
+
 
     def test_resolve_references(self):
         """Test keepass references can be resolved to values