krz/yoshi-cli

A password manager for the command line.

clone: git clone https://gitbay.org/krz/yoshi-cli.git

598e71b5ea1392f25cca0290c5544ab1135c37a2

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2024-11-02T21:58:31Z

add pylint workflow
 .github/workflows/pylint.yml |  23 +++++++
 Account.py                   |  16 +++++
 crypto.py                    |  45 ++++++++-----
 database.py                  |  77 ++++++++++++---------
 main.py                      |  87 ++++++++++++++++++------
 process.py                   | 157 ++++++++++++++++++++++++++++++++++---------
 6 files changed, 306 insertions(+), 99 deletions(-)

diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
new file mode 100644
index 0000000..d210abb
--- /dev/null
+++ b/.github/workflows/pylint.yml
@@ -0,0 +1,23 @@
+name: Pylint
+
+on: [push]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10"]
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install pylint pandas dash plotly.express
+    - name: Analysing the code with pylint
+      run: |
+        pylint -d R0801 $(git ls-files '*.py')
diff --git a/Account.py b/Account.py
index 11d4180..4e33518 100644
--- a/Account.py
+++ b/Account.py
@@ -1,7 +1,16 @@
+"""
+This script imports necessary modules for database interactions.
+
+Modules imported:
+    - database: A custom module providing database functionality.
+"""
+
 import database
 
 
 class Account:
+    """Represents a login account."""
+
     def __init__(self, uuid: str, application: str, username: str,
                  password: str, url: str) -> None:
         self.uuid = uuid
@@ -11,6 +20,7 @@ class Account:
         self.url = url
 
     def display_account(self) -> None:
+        """Print the account details."""
         print('ID:', self.uuid)
         print('Application:', self.application)
         print('Username:', self.username)
@@ -18,9 +28,15 @@ class Account:
         print('URL:', self.url)
 
     def save_account(self) -> None:
+        """Save the account details to the database."""
         database.create_account(
             self.uuid, self.application, self.username, self.password, self.url)
 
     def delete_account(self) -> bool:
+        """Delete the account from the database.
+
+        Returns:
+            bool: True if the deletion was successful.
+        """
         database.delete_account(self.uuid)
         return True
diff --git a/crypto.py b/crypto.py
index a96642b..9b0a423 100644
--- a/crypto.py
+++ b/crypto.py
@@ -1,30 +1,45 @@
+"""
+This module imports the Fernet symmetric encryption algorithm from the cryptography library.
+
+It allows for secure encryption and decryption of data using a secret key.
+"""
+
 from cryptography.fernet import Fernet
 
-vault_file = 'vault.sqlite'
+VAULT_FILE = 'vault.sqlite'
 
 
 def generate_key() -> bytes:
-    new_key = Fernet.generate_key()
-    return new_key
+    """Generates a new encryption key."""
+    return Fernet.generate_key()
 
 
 def load_key(key_file: str) -> bytes:
-    return open(key_file, 'rb').read()
+    """
+    Loads an existing encryption key from the file.
+
+    Args:
+        key_file (str): Path to the key file.
+    """
+    with open(key_file, 'rb') as key:
+        return key.read()
 
 
-def encrypt(key, filename=vault_file) -> None:
+def encrypt(key: bytes, filename: str = VAULT_FILE) -> None:
+    """Encrypts the data in the specified file using the provided key."""
     f = Fernet(key)
-    with open(filename, 'rb') as file:
-        file_data = file.read()
-    encrypted_data = f.encrypt(file_data)
-    with open(filename, 'wb') as file:
-        file.write(encrypted_data)
+    with open(filename, 'rb') as vault:
+        data = vault.read()
+    encrypted_data = f.encrypt(data)
+    with open(filename, 'wb') as vault:
+        vault.write(encrypted_data)
 
 
-def decrypt(key, filename=vault_file) -> None:
+def decrypt(key: bytes, filename: str = VAULT_FILE) -> None:
+    """Decrypts the data in the specified file using the provided key."""
     f = Fernet(key)
-    with open(filename, 'rb') as file:
-        encrypted_data = file.read()
+    with open(filename, 'rb') as vault:
+        encrypted_data = vault.read()
     decrypted_data = f.decrypt(encrypted_data)
-    with open(filename, 'wb') as file:
-        file.write(decrypted_data)
+    with open(filename, 'wb') as vault:
+        vault.write(decrypted_data)
diff --git a/database.py b/database.py
index 88d96b8..e1e2e78 100644
--- a/database.py
+++ b/database.py
@@ -1,34 +1,41 @@
-# Import Python modules
+"""
+This module provides a basic interface for connecting to and interacting with a SQLite database.
+It includes functions for creating connections, executing queries, and retrieving results.
+"""
+
 import sqlite3
 import sys
 import os
 
-# Specify the name of the vault database
-vault_decrypted = 'vault.sqlite'
-vault_encrypted = 'vault.sqlite.aes'
+VAULT_DECRYPTED = 'vault.sqlite'
+VAULT_ENCRYPTED = 'vault.sqlite.aes'
 
 
-# Create the accounts table inside the vault database
 def create_table() -> None:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Create the accounts table within the vault database."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' CREATE TABLE accounts (uuid text, application text, username text, password text, url text) ''')
+        ''' CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
+            username text, password text, url text) '''
+    )
     db_connection.commit()
     db_connection.close()
 
 
-# Check if the account table exists within the database
-def table_check() -> bool:
+def check_table() -> bool:
+    """Check if the 'accounts' table exists within the vault database."""
     check = False
-    db_connection = sqlite3.connect(vault_decrypted)
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' SELECT count(name) FROM sqlite_master WHERE type='table' AND name='accounts' ''')
+        ''' SELECT count(name) FROM sqlite_master WHERE type='table'
+            AND name='accounts' '''
+    )
     if cursor.fetchone()[0] != 1:
         user_choice = input(
             'Password vault does not exist. Would you like to create it now? (y/n): ')
-        if user_choice == 'y' or user_choice == 'Y':
+        if user_choice.lower() == 'y':
             create_table()
             check = True
         else:
@@ -40,44 +47,48 @@ def table_check() -> bool:
     return check
 
 
-# Add a new account to the database
-def create_account(uuid: str, application: str, username: str, password: str,
+def add_account(uuid: str, application: str, username: str, password: str,
                    url: str) -> None:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Add a new account within the vault database."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' INSERT INTO accounts VALUES (:uuid,:application,:username,:password,:url) ''',
-        {'uuid': uuid, 'application': application, 'username': username,
-         'password': password, 'url': url})
+        ''' INSERT INTO accounts VALUES (:uuid,:application,:username,
+            :password,:url) ''', {
+            'uuid': uuid, 'application': application, 'username': username,
+            'password': password, 'url': url
+        }
+    )
     db_connection.commit()
     db_connection.close()
 
 
-# Delete an account with a specified UUID
 def delete_account(uuid: str) -> None:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Delete an account within the vault database by its unique ID."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid})
+        ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
+    )
     db_connection.commit()
     db_connection.close()
 
 
-# Find a specific account by UUID
 def find_account(uuid: str) -> list:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Find an account within the vault database by its unique ID."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(
-        ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid})
+        ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
+    )
     account = cursor.fetchall()
     db_connection.close()
     return account
 
 
-# Return all accounts found within the database table
-# The `accounts` variable is a list of tuples
 def find_accounts() -> list:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Return all accounts stored within the vault database."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(''' SELECT * FROM accounts ''')
     accounts = cursor.fetchall()
@@ -86,22 +97,23 @@ def find_accounts() -> list:
 
 
 def update_account(field_name: str, new_value: str, uuid: str) -> None:
+    """Update an account within the vault database by its unique ID."""
     queries = {
         'application': 'UPDATE accounts SET application = :new_value WHERE uuid = :uuid',
         'username': 'UPDATE accounts SET username = :new_value WHERE uuid = :uuid',
         'password': 'UPDATE accounts SET password = :new_value WHERE uuid = :uuid',
         'url': 'UPDATE accounts SET url = :new_value WHERE uuid = :uuid'
     }
-    db_connection = sqlite3.connect(vault_decrypted)
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
-    cursor.execute(queries[field_name],
-                   {'new_value': new_value, 'uuid': uuid})
+    cursor.execute(queries[field_name], {'new_value': new_value, 'uuid': uuid})
     db_connection.commit()
     db_connection.close()
 
 
 def purge_table() -> None:
-    db_connection = sqlite3.connect(vault_decrypted)
+    """Purge the 'accounts' table within the vault database."""
+    db_connection = sqlite3.connect(VAULT_DECRYPTED)
     cursor = db_connection.cursor()
     cursor.execute(''' DROP TABLE accounts ''')
     db_connection.commit()
@@ -109,4 +121,5 @@ def purge_table() -> None:
 
 
 def purge_database() -> None:
-    os.remove(vault_decrypted)
+    """Purge the entire vault database."""
+    os.remove(VAULT_DECRYPTED)
diff --git a/main.py b/main.py
index 56755c1..4362b60 100644
--- a/main.py
+++ b/main.py
@@ -1,3 +1,9 @@
+"""
+This script uses argparse to parse command line arguments.
+
+It imports the required modules and sets up a parser with basic options for demonstration purposes.
+"""
+
 import argparse
 import crypto
 import database
@@ -5,47 +11,87 @@ import process
 
 if __name__ == '__main__':
     parser = argparse.ArgumentParser(
-        description='Manage your username and passwords via a convenient CLI vault.')
+        description='Manage your username and passwords via a convenient CLI vault.'
+    )
 
     # Top-level arguments
     group_one = parser.add_mutually_exclusive_group()
     group_one.add_argument(
-        '-n', '--new', help='Create a new account', action='store_true')
+        '-n', '--new',
+        help='Create a new account.',
+        action='store_true'
+    )
     group_one.add_argument(
-        '-l', '--list', help='List all saved accounts', action='store_true')
+        '-l', '--list',
+        help='List all saved accounts.',
+        action='store_true'
+    )
     group_one.add_argument(
-        '-e', '--edit', help='Edit a saved account', action='store_true')
+        '-e', '--edit',
+        help='Edit a saved account.',
+        action='store_true'
+    )
     group_one.add_argument(
-        '-d', '--delete', help='Delete a saved account', action='store_true')
+        '-d', '--delete',
+        help='Delete a saved account.',
+        action='store_true'
+    )
     group_one.add_argument(
-        '--purge', help='Purge all accounts and delete the vault',
-        action='store_true')
+        '--purge',
+        help=(
+            'Purge all accounts and delete the vault. '
+            '(Caution: this will irreversibly destroy your data.)'
+        ),
+        action='store_true'
+    )
     group_one.add_argument(
-        '--encrypt', help='Encrypt the vault', action='store_true')
+        '--encrypt',
+        help='Encrypt the vault.',
+        action='store_true'
+    )
     group_one.add_argument(
-        '--decrypt', help='Decrypt the vault', action='store_true')
+        '--decrypt',
+        help='Decrypt the vault.',
+        action='store_true'
+    )
 
     # Encryption flags
     group_two = parser.add_mutually_exclusive_group()
     group_two.add_argument(
         '-g', '--generate',
-        help='When using the --encrypt option, generate a new encryption key',
-        action='store_true')
+        help=(
+            'When using the --encrypt option, generate a new encryption key.'
+        ),
+        action='store_true'
+    )
     group_two.add_argument(
         '-k', '--keyfile',
-        help='When using the --encrypt or --decrypt options, specify an existing key file path',
-        action='store', nargs=1, type=str)
+        help='Path to existing key file.',
+        action='store',
+        nargs=1,
+        type=str
+    )
 
     # Edit flags
     group_three = parser.add_argument_group()
     group_three.add_argument(
         '-u', '--uuid',
-        help='When using the --edit or --delete options, provide the account UUID',
-        action='store', nargs=1, type=str)
+        help=(
+            'When using the --edit or --delete options, provide the account UUID.'
+        ),
+        action='store',
+        nargs=1,
+        type=str
+    )
     group_three.add_argument(
         '-f', '--field',
-        help='When using the --edit option, provide the field to edit',
-        action='store', nargs=1, type=int)
+        help=(
+            'When using the --edit option, specify the field to edit (integer index).'
+        ),
+        action='store',
+        nargs=1,
+        type=int
+    )
 
     args = parser.parse_args()
 
@@ -59,7 +105,9 @@ if __name__ == '__main__':
         if args.generate:
             key = crypto.generate_key()
             print(
-                'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT YOUR DATA WITHOUT IT!')
+                'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT '
+                'YOUR DATA WITHOUT IT!'
+            )
             print(key.decode())
             print('\n')
         else:
@@ -81,4 +129,5 @@ if __name__ == '__main__':
             process.purge_accounts()
         else:
             raise TypeError(
-                'Please specify a command or use the --help flag for more information.')
+                'Please specify a command or use the --help flag for more information.'
+            )
diff --git a/process.py b/process.py
index aa2ad76..6e84dfe 100644
--- a/process.py
+++ b/process.py
@@ -1,4 +1,29 @@
-from Account import Account
+"""
+Password Vault Manager
+
+This script provides various functions for managing password vaults.
+It allows users to create, list, edit and delete accounts.
+
+The `Account` class represents an individual account, with attributes for
+the application name, username, password, and URL. The database module is used
+to interact with the SQLite database file (`vault.sqlite`) that stores the
+accounts data.
+
+Functions:
+    generate_characters(n): generates a list of random characters
+    shuffle_characters(characters): shuffles the characters to create a password
+    generate_passphrase(n, sep): generates an XKCD-style passphrase with n words and separator
+    list_accounts(): lists all saved accounts in the database
+    delete_account(uuid): deletes an account by its UUID
+    purge_accounts(): purges the entire database (irreversible)
+    create_account(): creates a new account by prompting user for details
+    edit_account(uuid, edit_parameter): edits an existing account's details
+
+Usage:
+    Run this script in your terminal to access these functions.
+"""
+
+from account import Account
 import database
 from string import ascii_letters, punctuation, digits
 import random
@@ -6,39 +31,70 @@ import uuid
 from prettytable import PrettyTable
 
 
-# Generate a list of characters
 def generate_characters(n: int) -> list:
-    characters = list()
+    """
+    Generates a list of n random characters from the set of ASCII letters,
+    punctuation and digits.
+
+    Args:
+        n (int): The number of characters to generate
+
+    Returns:
+        list: A list of n random characters
+    """
+    characters = []
     password_format = ascii_letters + punctuation + digits
-    for x in range(n):
+    for _ in range(n):
         characters.append(random.choice(password_format))
     return characters
 
 
-# Randomly shuffle the characters
 def shuffle_characters(characters: list) -> str:
+    """
+    Shuffles the characters to create a password.
+
+    Args:
+        characters (list): The list of characters
+
+    Returns:
+        str: A string representation of the shuffled characters
+    """
     random.shuffle(characters)
     character_string = ''.join(characters)
     return character_string
 
 
-# Generate a combination of passphrases
 def generate_passphrase(n: int, sep: str) -> str:
+    """
+    Generates an XKCD-style passphrase with n words and separator.
+
+    Args:
+        n (int): The number of words to include
+        sep (str): The separator symbol
+
+    Returns:
+        str: A string representation of the passphrase
+    """
     phrases = []
     lucky_number = random.choice(range(0, n))
-    for x in range(0, n):
+    for _ in range(n):
         line = random.choice(open('wordlist.txt').readlines())
         line = line.replace('\n', '')
-        if x == lucky_number:
-            phrases.append(line.strip().capitalize() + str(x))
+        if _ == lucky_number:
+            phrases.append(line.strip().capitalize() + str(_))
         else:
             phrases.append(line.strip().capitalize())
     passphrase = sep.join(phrases)
     return passphrase
 
 
-# List all saved accounts
 def list_accounts() -> None:
+    """
+    Lists all saved accounts in the database.
+
+    Returns:
+        None
+    """
     accounts = database.find_accounts()
     t = PrettyTable(['UUID', 'Application', 'Username', 'Password', 'URL'])
     for account in accounts:
@@ -46,10 +102,18 @@ def list_accounts() -> None:
     print(t)
 
 
-# Delete a single account by UUID
 def delete_account(uuid: str) -> None:
+    """
+    Deletes an account by its UUID.
+
+    Args:
+        uuid (str): The UUID of the account to delete
+
+    Returns:
+        None
+    """
     account_record = database.find_account(uuid)
-    account: Account = Account(account_record[0][0],
+    account = Account(account_record[0][0],
                                account_record[0][1],
                                account_record[0][2],
                                account_record[0][3],
@@ -58,52 +122,79 @@ def delete_account(uuid: str) -> None:
         print('Account successfully deleted.')
 
 
-# Purge the `accounts` table and `vault.sqlite` file
 def purge_accounts() -> None:
+    """
+    Purges the entire database (irreversible).
+
+    Returns:
+        None
+    """
     check = input(
         'Are you absolutely sure you want to delete your password vault? This action is irreversible. (y/n): ')
-    if check:
+    if check.lower() == 'y':
         database.purge_table()
         database.purge_database()
         print('The password vault has been purged. You may now exit or create a new one.')
 
 
-# Request user input and create an account
 def create_account() -> None:
+    """
+    Creates a new account by prompting user for details.
+
+    Returns:
+        None
+    """
     application_string = input('Please enter a name for this account: ')
     username_string = input('Please enter your username for this account: ')
     url_string = input('(Optional) Please enter a URL for this account: ')
+
     password_type = input(
-        '''Do you want a random character password (p), an XKCD-style passphrase 
-(x), or a custom password (c)? (p|x|c): ''')
-    if password_type == "x" or password_type == "xkcd":
+        '''Do you want a random character password (p), an XKCD-style passphrase
+(x), or a custom password (c)? (p|x|c): '''
+    )
+    if password_type not in ['p', 'x', 'c']:
+        print('Error: Invalid choice. Please choose p, x, or c.')
+        return
+
+    if password_type == 'x':
         password_length = int(
-            input('Please enter number of words to include (min. 2): '))
-        password_separator = input(
-            'Please enter your desired separator symbol (_, -, ~, etc.: ')
+            input('Please enter number of words to include (min. 2): ')
+        )
         if password_length < 3:
             print('Error: Your passphrase length must be at least 3 words.')
-        else:
-            password_string = generate_passphrase(
-                password_length, password_separator)
-    elif password_type == "p":
+            return
+        password_separator = input(
+            'Please enter your desired separator symbol (_,-, ~, etc.): '
+        )
+        password_string = generate_passphrase(password_length, password_separator)
+    elif password_type == 'p':
         password_length = int(
-            input('Please enter your desired password length (min. 8): '))
+            input('Please enter your desired password length (min. 8): ')
+        )
         if password_length < 8:
             print('Error: Your password length must be at least 8 characters.')
-        else:
-            password_characters = generate_characters(password_length)
-            password_string = shuffle_characters(password_characters)
+            return
+        password_string = generate_password(password_length)
     else:
         password_string = input('Please enter your desired password: ')
+
     account = Account(str(uuid.uuid4()), application_string,
                       username_string, password_string, url_string)
     account.save_account()
     print('Account saved to the vault. Use `--list` to see all saved accounts.')
 
 
-# Allow users to edit any account info except the UUID
+
 def edit_account(uuid: str, edit_parameter: int) -> None:
+    """
+    Allow users to edit any account information except the UUID.
+
+    Args:
+        uuid (str): Unique identifier of the account.
+        edit_parameter (int): Parameter indicating which field to edit.
+            Valid values are 1 for application name, 2 for username,
+            3 for password, and 4 for URL.
+    """
     if edit_parameter == 1:
         field_name = 'application'
         new_value = input('Please enter your desired Application name: ')
@@ -113,10 +204,10 @@ def edit_account(uuid: str, edit_parameter: int) -> None:
     elif edit_parameter == 3:
         field_name = 'password'
         type_check = input(
-            'Do you want a new random password or to enter a custom password? (random/custom): ')
+            'Do you want a new random password or to enter a custom password? '
+            '(random/custom): ').lower()
         if type_check == 'random':
-            password_length = int(
-                input('Please enter your desired password length: '))
+            password_length = int(input('Please enter your desired password length: '))
             if password_length < 8:
                 print('Error: Your password length must be at least 8 characters.')
             else: