Repository: rotki/rotki

Issue:
Password handling
## Problem Definition

passwords exceeding a certain length or containing special chars throw (500). With a 49 char and without special chars, everything is fine

### Solution Definition

- [ ] show max pw length in create screen. Show excluded/included chars. Target user group of rotki likely generates pws.
- [ ] show if entered password contains "illicit" chars and disable "create" button.
- [ ] show "passwords don't match" hint only after pressing "create" button.



Discussion:
Hey @kilrau thank you so much for spotting this. Had no idea we had such a problem. We will handle it!
I have not tested it yet but can you provide few examples of invalid passwords? Is it only the 50 chars limit? Or also some characters not allowed? We are only limited by what the underlying sqlcipher library permits.

Resolving patch:
diff --git a/rotkehlchen/db/dbhandler.py b/rotkehlchen/db/dbhandler.py
--- a/rotkehlchen/db/dbhandler.py
+++ b/rotkehlchen/db/dbhandler.py
@@ -87,6 +87,14 @@
 DBTupleType = Literal['trade', 'asset_movement', 'margin_position', 'ethereum_transaction']
 
 
+def _protect_password_sqlcipher(password: str) -> str:
+    """A double quote in the password would close the string. To escape it double it
+
+    source: https://stackoverflow.com/a/603579/110395
+"""
+    return password.replace(r'"', r'""')
+
+
 def detect_sqlcipher_version() -> int:
     """Returns the major part of the version of the system's sqlcipher package"""
     conn = sqlcipher.connect(':memory:')  # pylint: disable=no-member
@@ -291,9 +299,12 @@ def connect(self, password: str) -> None:
             os.path.join(self.user_data_dir, 'rotkehlchen.db'),
         )
         self.conn.text_factory = str
-        self.conn.executescript('PRAGMA key="{}";'.format(password))
+        password_for_sqlcipher = _protect_password_sqlcipher(password)
+        # __import__("pdb").set_trace()
+
+        self.conn.executescript(f'PRAGMA key="{password_for_sqlcipher}";')
         if self.sqlcipher_version == 3:
-            script = f'PRAGMA key="{password}"; PRAGMA kdf_iter={KDF_ITER};'
+            script = f'PRAGMA key="{password_for_sqlcipher}"; PRAGMA kdf_iter={KDF_ITER};'
             self.conn.executescript(script)
         self.conn.execute('PRAGMA foreign_keys=ON')
 
@@ -306,7 +317,8 @@ def upgrade_db_sqlcipher_3_to_4(self, password: str) -> Tuple[bool, str]:
         success = True
         msg = ''
         self.conn.text_factory = str
-        script = f'PRAGMA KEY="{password}";PRAGMA cipher_migrate;'
+        password_for_sqlcipher = _protect_password_sqlcipher(password)
+        script = f'PRAGMA KEY="{password_for_sqlcipher}";PRAGMA cipher_migrate;'
         try:
             self.conn.executescript(script)
             self.conn.executescript(DB_SCRIPT_CREATE_TABLES)
@@ -351,7 +363,8 @@ def import_unencrypted(self, unencrypted_db_data: bytes, password: str) -> None:
 
             # Now attach to the unencrypted DB and copy it to our DB and encrypt it
             self.conn = sqlcipher.connect(tempdbpath)  # pylint: disable=no-member
-            script = f'ATTACH DATABASE "{rdbpath}" AS encrypted KEY "{password}";'
+            password_for_sqlcipher = _protect_password_sqlcipher(password)
+            script = f'ATTACH DATABASE "{rdbpath}" AS encrypted KEY "{password_for_sqlcipher}";'
             if self.sqlcipher_version == 3:
                 script += f'PRAGMA encrypted.kdf_iter={KDF_ITER};'
             script += 'SELECT sqlcipher_export("encrypted");DETACH DATABASE encrypted;'


Test patch:
diff --git a/rotkehlchen/tests/test_users.py b/rotkehlchen/tests/test_users.py
new file mode 100644
--- /dev/null
+++ b/rotkehlchen/tests/test_users.py
@@ -0,0 +1,54 @@
+import string
+
+from rotkehlchen.data_handler import DataHandler
+
+
+def _user_creation_and_login(username, password, data_dir, msg_aggregator):
+    handler = DataHandler(
+        data_directory=data_dir,
+        msg_aggregator=msg_aggregator,
+    )
+    filepath = handler.unlock(username=username, password=password, create_new=True)
+    assert filepath is not None
+    # Also login as non-new user with same password
+    handler.logout()
+    filepath = handler.unlock(username=username, password=password, create_new=False)
+    assert filepath is not None
+
+
+def test_user_long_password(data_dir, function_scope_messages_aggregator):
+    """Test that very long password work. https://github.com/rotki/rotki/issues/805"""
+    username = 'foo'
+    password = 'dadsadsadsdsasadsadsadsadsadsadsadhkhdkjasd#$%$%*)(\\aasdasdsadjsakdhjaskdhkjsadhkjsadhsdadsadjksajdlskajdskldjslkdjklasjdlsadjsadj4324@#@qweioqweiCZCK#$a'  # noqa: E501
+    _user_creation_and_login(
+        username=username,
+        password=password,
+        data_dir=data_dir,
+        msg_aggregator=function_scope_messages_aggregator,
+    )
+
+
+def test_user_password_with_double_quote(data_dir, function_scope_messages_aggregator):
+    """Test that a password containing " is accepted.
+
+    Probably what caused https://github.com/rotki/rotki/issues/805 to be reported"""
+    username = 'foo'
+    password = 'pass"word'
+    _user_creation_and_login(
+        username=username,
+        password=password,
+        data_dir=data_dir,
+        msg_aggregator=function_scope_messages_aggregator,
+    )
+
+
+def test_user_password_with_all_ascii(data_dir, function_scope_messages_aggregator):
+    """Test that a password containing all ASCII characters is accepted"""
+    username = 'foo'
+    password = string.printable
+    _user_creation_and_login(
+        username=username,
+        password=password,
+        data_dir=data_dir,
+        msg_aggregator=function_scope_messages_aggregator,
+    )