From 598e71b5ea1392f25cca0290c5544ab1135c37a2 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sat, 2 Nov 2024 16:58:31 -0500 Subject: 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(-) create mode 100644 .github/workflows/pylint.yml 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: -- cgit v1.2.3-70-g09d2