krz/yoshi-cli

Archived

A password manager for the command line. cli password-manager terminal

Commit 598e71b5ea

598e71b5ea1392f25cca0290c5544ab1135c37a2

parent: 0bef69a049

Verified · cmc

cmc <hello@cleberg.net> · 2024-11-02 21:58 UTC

add pylint workflow

Layout: unified · split

.github/workflows/pylint.yml added +23
@@ -0,0 +1,23 @@
1name: Pylint
2
3on: [push]
4
5jobs:
6 build:
7 runs-on: ubuntu-latest
8 strategy:
9 matrix:
10 python-version: ["3.8", "3.9", "3.10"]
11 steps:
12 - uses: actions/checkout@v4
13 - name: Set up Python ${{ matrix.python-version }}
14 uses: actions/setup-python@v4
15 with:
16 python-version: ${{ matrix.python-version }}
17 - name: Install dependencies
18 run: |
19 python -m pip install --upgrade pip
20 pip install pylint pandas dash plotly.express
21 - name: Analysing the code with pylint
22 run: |
23 pylint -d R0801 $(git ls-files '*.py')
Account.py +16
@@ -1,7 +1,16 @@
1"""
2This script imports necessary modules for database interactions.
3
4Modules imported:
5 - database: A custom module providing database functionality.
6"""
7
18import database
29
310
411class Account:
12 """Represents a login account."""
13
514 def __init__(self, uuid: str, application: str, username: str,
615 password: str, url: str) -> None:
716 self.uuid = uuid
@@ -11,6 +20,7 @@ class Account:
1120 self.url = url
1221
1322 def display_account(self) -> None:
23 """Print the account details."""
1424 print('ID:', self.uuid)
1525 print('Application:', self.application)
1626 print('Username:', self.username)
@@ -18,9 +28,15 @@ class Account:
1828 print('URL:', self.url)
1929
2030 def save_account(self) -> None:
31 """Save the account details to the database."""
2132 database.create_account(
2233 self.uuid, self.application, self.username, self.password, self.url)
2334
2435 def delete_account(self) -> bool:
36 """Delete the account from the database.
37
38 Returns:
39 bool: True if the deletion was successful.
40 """
2541 database.delete_account(self.uuid)
2642 return True
crypto.py +30 −15
@@ -1,30 +1,45 @@
1"""
2This module imports the Fernet symmetric encryption algorithm from the cryptography library.
3
4It allows for secure encryption and decryption of data using a secret key.
5"""
6
17from cryptography.fernet import Fernet
28
3vault_file = 'vault.sqlite'
9VAULT_FILE = 'vault.sqlite'
410
511
612def generate_key() -> bytes:
7 new_key = Fernet.generate_key()
8 return new_key
13 """Generates a new encryption key."""
14 return Fernet.generate_key()
915
1016
1117def load_key(key_file: str) -> bytes:
12 return open(key_file, 'rb').read()
18 """
19 Loads an existing encryption key from the file.
20
21 Args:
22 key_file (str): Path to the key file.
23 """
24 with open(key_file, 'rb') as key:
25 return key.read()
1326
1427
15def encrypt(key, filename=vault_file) -> None:
28def encrypt(key: bytes, filename: str = VAULT_FILE) -> None:
29 """Encrypts the data in the specified file using the provided key."""
1630 f = Fernet(key)
17 with open(filename, 'rb') as file:
18 file_data = file.read()
19 encrypted_data = f.encrypt(file_data)
20 with open(filename, 'wb') as file:
21 file.write(encrypted_data)
31 with open(filename, 'rb') as vault:
32 data = vault.read()
33 encrypted_data = f.encrypt(data)
34 with open(filename, 'wb') as vault:
35 vault.write(encrypted_data)
2236
2337
24def decrypt(key, filename=vault_file) -> None:
38def decrypt(key: bytes, filename: str = VAULT_FILE) -> None:
39 """Decrypts the data in the specified file using the provided key."""
2540 f = Fernet(key)
26 with open(filename, 'rb') as file:
27 encrypted_data = file.read()
41 with open(filename, 'rb') as vault:
42 encrypted_data = vault.read()
2843 decrypted_data = f.decrypt(encrypted_data)
29 with open(filename, 'wb') as file:
30 file.write(decrypted_data)
44 with open(filename, 'wb') as vault:
45 vault.write(decrypted_data)
database.py +45 −32
@@ -1,34 +1,41 @@
1# Import Python modules
1"""
2This module provides a basic interface for connecting to and interacting with a SQLite database.
3It includes functions for creating connections, executing queries, and retrieving results.
4"""
5
26import sqlite3
37import sys
48import os
59
6# Specify the name of the vault database
7vault_decrypted = 'vault.sqlite'
8vault_encrypted = 'vault.sqlite.aes'
10VAULT_DECRYPTED = 'vault.sqlite'
11VAULT_ENCRYPTED = 'vault.sqlite.aes'
912
1013
11# Create the accounts table inside the vault database
1214def create_table() -> None:
13 db_connection = sqlite3.connect(vault_decrypted)
15 """Create the accounts table within the vault database."""
16 db_connection = sqlite3.connect(VAULT_DECRYPTED)
1417 cursor = db_connection.cursor()
1518 cursor.execute(
16 ''' CREATE TABLE accounts (uuid text, application text, username text, password text, url text) ''')
19 ''' CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
20 username text, password text, url text) '''
21 )
1722 db_connection.commit()
1823 db_connection.close()
1924
2025
21# Check if the account table exists within the database
22def table_check() -> bool:
26def check_table() -> bool:
27 """Check if the 'accounts' table exists within the vault database."""
2328 check = False
24 db_connection = sqlite3.connect(vault_decrypted)
29 db_connection = sqlite3.connect(VAULT_DECRYPTED)
2530 cursor = db_connection.cursor()
2631 cursor.execute(
27 ''' SELECT count(name) FROM sqlite_master WHERE type='table' AND name='accounts' ''')
32 ''' SELECT count(name) FROM sqlite_master WHERE type='table'
33 AND name='accounts' '''
34 )
2835 if cursor.fetchone()[0] != 1:
2936 user_choice = input(
3037 'Password vault does not exist. Would you like to create it now? (y/n): ')
31 if user_choice == 'y' or user_choice == 'Y':
38 if user_choice.lower() == 'y':
3239 create_table()
3340 check = True
3441 else:
@@ -40,44 +47,48 @@ def table_check() -> bool:
4047 return check
4148
4249
43# Add a new account to the database
44def create_account(uuid: str, application: str, username: str, password: str,
50def add_account(uuid: str, application: str, username: str, password: str,
4551 url: str) -> None:
46 db_connection = sqlite3.connect(vault_decrypted)
52 """Add a new account within the vault database."""
53 db_connection = sqlite3.connect(VAULT_DECRYPTED)
4754 cursor = db_connection.cursor()
4855 cursor.execute(
49 ''' INSERT INTO accounts VALUES (:uuid,:application,:username,:password,:url) ''',
50 {'uuid': uuid, 'application': application, 'username': username,
51 'password': password, 'url': url})
56 ''' INSERT INTO accounts VALUES (:uuid,:application,:username,
57 :password,:url) ''', {
58 'uuid': uuid, 'application': application, 'username': username,
59 'password': password, 'url': url
60 }
61 )
5262 db_connection.commit()
5363 db_connection.close()
5464
5565
56# Delete an account with a specified UUID
5766def delete_account(uuid: str) -> None:
58 db_connection = sqlite3.connect(vault_decrypted)
67 """Delete an account within the vault database by its unique ID."""
68 db_connection = sqlite3.connect(VAULT_DECRYPTED)
5969 cursor = db_connection.cursor()
6070 cursor.execute(
61 ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid})
71 ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
72 )
6273 db_connection.commit()
6374 db_connection.close()
6475
6576
66# Find a specific account by UUID
6777def find_account(uuid: str) -> list:
68 db_connection = sqlite3.connect(vault_decrypted)
78 """Find an account within the vault database by its unique ID."""
79 db_connection = sqlite3.connect(VAULT_DECRYPTED)
6980 cursor = db_connection.cursor()
7081 cursor.execute(
71 ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid})
82 ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
83 )
7284 account = cursor.fetchall()
7385 db_connection.close()
7486 return account
7587
7688
77# Return all accounts found within the database table
78# The `accounts` variable is a list of tuples
7989def find_accounts() -> list:
80 db_connection = sqlite3.connect(vault_decrypted)
90 """Return all accounts stored within the vault database."""
91 db_connection = sqlite3.connect(VAULT_DECRYPTED)
8192 cursor = db_connection.cursor()
8293 cursor.execute(''' SELECT * FROM accounts ''')
8394 accounts = cursor.fetchall()
@@ -86,22 +97,23 @@ def find_accounts() -> list:
8697
8798
8899def update_account(field_name: str, new_value: str, uuid: str) -> None:
100 """Update an account within the vault database by its unique ID."""
89101 queries = {
90102 'application': 'UPDATE accounts SET application = :new_value WHERE uuid = :uuid',
91103 'username': 'UPDATE accounts SET username = :new_value WHERE uuid = :uuid',
92104 'password': 'UPDATE accounts SET password = :new_value WHERE uuid = :uuid',
93105 'url': 'UPDATE accounts SET url = :new_value WHERE uuid = :uuid'
94106 }
95 db_connection = sqlite3.connect(vault_decrypted)
107 db_connection = sqlite3.connect(VAULT_DECRYPTED)
96108 cursor = db_connection.cursor()
97 cursor.execute(queries[field_name],
98 {'new_value': new_value, 'uuid': uuid})
109 cursor.execute(queries[field_name], {'new_value': new_value, 'uuid': uuid})
99110 db_connection.commit()
100111 db_connection.close()
101112
102113
103114def purge_table() -> None:
104 db_connection = sqlite3.connect(vault_decrypted)
115 """Purge the 'accounts' table within the vault database."""
116 db_connection = sqlite3.connect(VAULT_DECRYPTED)
105117 cursor = db_connection.cursor()
106118 cursor.execute(''' DROP TABLE accounts ''')
107119 db_connection.commit()
@@ -109,4 +121,5 @@ def purge_table() -> None:
109121
110122
111123def purge_database() -> None:
112 os.remove(vault_decrypted)
124 """Purge the entire vault database."""
125 os.remove(VAULT_DECRYPTED)
main.py +68 −19
@@ -1,3 +1,9 @@
1"""
2This script uses argparse to parse command line arguments.
3
4It imports the required modules and sets up a parser with basic options for demonstration purposes.
5"""
6
17import argparse
28import crypto
39import database
@@ -5,47 +11,87 @@ import process
511
612if __name__ == '__main__':
713 parser = argparse.ArgumentParser(
8 description='Manage your username and passwords via a convenient CLI vault.')
14 description='Manage your username and passwords via a convenient CLI vault.'
15 )
916
1017 # Top-level arguments
1118 group_one = parser.add_mutually_exclusive_group()
1219 group_one.add_argument(
13 '-n', '--new', help='Create a new account', action='store_true')
20 '-n', '--new',
21 help='Create a new account.',
22 action='store_true'
23 )
1424 group_one.add_argument(
15 '-l', '--list', help='List all saved accounts', action='store_true')
25 '-l', '--list',
26 help='List all saved accounts.',
27 action='store_true'
28 )
1629 group_one.add_argument(
17 '-e', '--edit', help='Edit a saved account', action='store_true')
30 '-e', '--edit',
31 help='Edit a saved account.',
32 action='store_true'
33 )
1834 group_one.add_argument(
19 '-d', '--delete', help='Delete a saved account', action='store_true')
35 '-d', '--delete',
36 help='Delete a saved account.',
37 action='store_true'
38 )
2039 group_one.add_argument(
21 '--purge', help='Purge all accounts and delete the vault',
22 action='store_true')
40 '--purge',
41 help=(
42 'Purge all accounts and delete the vault. '
43 '(Caution: this will irreversibly destroy your data.)'
44 ),
45 action='store_true'
46 )
2347 group_one.add_argument(
24 '--encrypt', help='Encrypt the vault', action='store_true')
48 '--encrypt',
49 help='Encrypt the vault.',
50 action='store_true'
51 )
2552 group_one.add_argument(
26 '--decrypt', help='Decrypt the vault', action='store_true')
53 '--decrypt',
54 help='Decrypt the vault.',
55 action='store_true'
56 )
2757
2858 # Encryption flags
2959 group_two = parser.add_mutually_exclusive_group()
3060 group_two.add_argument(
3161 '-g', '--generate',
32 help='When using the --encrypt option, generate a new encryption key',
33 action='store_true')
62 help=(
63 'When using the --encrypt option, generate a new encryption key.'
64 ),
65 action='store_true'
66 )
3467 group_two.add_argument(
3568 '-k', '--keyfile',
36 help='When using the --encrypt or --decrypt options, specify an existing key file path',
37 action='store', nargs=1, type=str)
69 help='Path to existing key file.',
70 action='store',
71 nargs=1,
72 type=str
73 )
3874
3975 # Edit flags
4076 group_three = parser.add_argument_group()
4177 group_three.add_argument(
4278 '-u', '--uuid',
43 help='When using the --edit or --delete options, provide the account UUID',
44 action='store', nargs=1, type=str)
79 help=(
80 'When using the --edit or --delete options, provide the account UUID.'
81 ),
82 action='store',
83 nargs=1,
84 type=str
85 )
4586 group_three.add_argument(
4687 '-f', '--field',
47 help='When using the --edit option, provide the field to edit',
48 action='store', nargs=1, type=int)
88 help=(
89 'When using the --edit option, specify the field to edit (integer index).'
90 ),
91 action='store',
92 nargs=1,
93 type=int
94 )
4995
5096 args = parser.parse_args()
5197
@@ -59,7 +105,9 @@ if __name__ == '__main__':
59105 if args.generate:
60106 key = crypto.generate_key()
61107 print(
62 'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT YOUR DATA WITHOUT IT!')
108 'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT '
109 'YOUR DATA WITHOUT IT!'
110 )
63111 print(key.decode())
64112 print('\n')
65113 else:
@@ -81,4 +129,5 @@ if __name__ == '__main__':
81129 process.purge_accounts()
82130 else:
83131 raise TypeError(
84 'Please specify a command or use the --help flag for more information.')
132 'Please specify a command or use the --help flag for more information.'
133 )
process.py +124 −33
@@ -1,4 +1,29 @@
1from Account import Account
1"""
2Password Vault Manager
3
4This script provides various functions for managing password vaults.
5It allows users to create, list, edit and delete accounts.
6
7The `Account` class represents an individual account, with attributes for
8the application name, username, password, and URL. The database module is used
9to interact with the SQLite database file (`vault.sqlite`) that stores the
10accounts data.
11
12Functions:
13 generate_characters(n): generates a list of random characters
14 shuffle_characters(characters): shuffles the characters to create a password
15 generate_passphrase(n, sep): generates an XKCD-style passphrase with n words and separator
16 list_accounts(): lists all saved accounts in the database
17 delete_account(uuid): deletes an account by its UUID
18 purge_accounts(): purges the entire database (irreversible)
19 create_account(): creates a new account by prompting user for details
20 edit_account(uuid, edit_parameter): edits an existing account's details
21
22Usage:
23 Run this script in your terminal to access these functions.
24"""
25
26from account import Account
227import database
328from string import ascii_letters, punctuation, digits
429import random
@@ -6,39 +31,70 @@ import uuid
631from prettytable import PrettyTable
732
833
9# Generate a list of characters
1034def generate_characters(n: int) -> list:
11 characters = list()
35 """
36 Generates a list of n random characters from the set of ASCII letters,
37 punctuation and digits.
38
39 Args:
40 n (int): The number of characters to generate
41
42 Returns:
43 list: A list of n random characters
44 """
45 characters = []
1246 password_format = ascii_letters + punctuation + digits
13 for x in range(n):
47 for _ in range(n):
1448 characters.append(random.choice(password_format))
1549 return characters
1650
1751
18# Randomly shuffle the characters
1952def shuffle_characters(characters: list) -> str:
53 """
54 Shuffles the characters to create a password.
55
56 Args:
57 characters (list): The list of characters
58
59 Returns:
60 str: A string representation of the shuffled characters
61 """
2062 random.shuffle(characters)
2163 character_string = ''.join(characters)
2264 return character_string
2365
2466
25# Generate a combination of passphrases
2667def generate_passphrase(n: int, sep: str) -> str:
68 """
69 Generates an XKCD-style passphrase with n words and separator.
70
71 Args:
72 n (int): The number of words to include
73 sep (str): The separator symbol
74
75 Returns:
76 str: A string representation of the passphrase
77 """
2778 phrases = []
2879 lucky_number = random.choice(range(0, n))
29 for x in range(0, n):
80 for _ in range(n):
3081 line = random.choice(open('wordlist.txt').readlines())
3182 line = line.replace('\n', '')
32 if x == lucky_number:
33 phrases.append(line.strip().capitalize() + str(x))
83 if _ == lucky_number:
84 phrases.append(line.strip().capitalize() + str(_))
3485 else:
3586 phrases.append(line.strip().capitalize())
3687 passphrase = sep.join(phrases)
3788 return passphrase
3889
3990
40# List all saved accounts
4191def list_accounts() -> None:
92 """
93 Lists all saved accounts in the database.
94
95 Returns:
96 None
97 """
4298 accounts = database.find_accounts()
4399 t = PrettyTable(['UUID', 'Application', 'Username', 'Password', 'URL'])
44100 for account in accounts:
@@ -46,10 +102,18 @@ def list_accounts() -> None:
46102 print(t)
47103
48104
49# Delete a single account by UUID
50105def delete_account(uuid: str) -> None:
106 """
107 Deletes an account by its UUID.
108
109 Args:
110 uuid (str): The UUID of the account to delete
111
112 Returns:
113 None
114 """
51115 account_record = database.find_account(uuid)
52 account: Account = Account(account_record[0][0],
116 account = Account(account_record[0][0],
53117 account_record[0][1],
54118 account_record[0][2],
55119 account_record[0][3],
@@ -58,52 +122,79 @@ def delete_account(uuid: str) -> None:
58122 print('Account successfully deleted.')
59123
60124
61# Purge the `accounts` table and `vault.sqlite` file
62125def purge_accounts() -> None:
126 """
127 Purges the entire database (irreversible).
128
129 Returns:
130 None
131 """
63132 check = input(
64133 'Are you absolutely sure you want to delete your password vault? This action is irreversible. (y/n): ')
65 if check:
134 if check.lower() == 'y':
66135 database.purge_table()
67136 database.purge_database()
68137 print('The password vault has been purged. You may now exit or create a new one.')
69138
70139
71# Request user input and create an account
72140def create_account() -> None:
141 """
142 Creates a new account by prompting user for details.
143
144 Returns:
145 None
146 """
73147 application_string = input('Please enter a name for this account: ')
74148 username_string = input('Please enter your username for this account: ')
75149 url_string = input('(Optional) Please enter a URL for this account: ')
150
76151 password_type = input(
77 '''Do you want a random character password (p), an XKCD-style passphrase
78(x), or a custom password (c)? (p|x|c): ''')
79 if password_type == "x" or password_type == "xkcd":
152 '''Do you want a random character password (p), an XKCD-style passphrase
153(x), or a custom password (c)? (p|x|c): '''
154 )
155 if password_type not in ['p', 'x', 'c']:
156 print('Error: Invalid choice. Please choose p, x, or c.')
157 return
158
159 if password_type == 'x':
80160 password_length = int(
81 input('Please enter number of words to include (min. 2): '))
82 password_separator = input(
83 'Please enter your desired separator symbol (_, -, ~, etc.: ')
161 input('Please enter number of words to include (min. 2): ')
162 )
84163 if password_length < 3:
85164 print('Error: Your passphrase length must be at least 3 words.')
86 else:
87 password_string = generate_passphrase(
88 password_length, password_separator)
89 elif password_type == "p":
165 return
166 password_separator = input(
167 'Please enter your desired separator symbol (_,-, ~, etc.): '
168 )
169 password_string = generate_passphrase(password_length, password_separator)
170 elif password_type == 'p':
90171 password_length = int(
91 input('Please enter your desired password length (min. 8): '))
172 input('Please enter your desired password length (min. 8): ')
173 )
92174 if password_length < 8:
93175 print('Error: Your password length must be at least 8 characters.')
94 else:
95 password_characters = generate_characters(password_length)
96 password_string = shuffle_characters(password_characters)
176 return
177 password_string = generate_password(password_length)
97178 else:
98179 password_string = input('Please enter your desired password: ')
180
99181 account = Account(str(uuid.uuid4()), application_string,
100182 username_string, password_string, url_string)
101183 account.save_account()
102184 print('Account saved to the vault. Use `--list` to see all saved accounts.')
103185
104186
105# Allow users to edit any account info except the UUID
187
106188def edit_account(uuid: str, edit_parameter: int) -> None:
189 """
190 Allow users to edit any account information except the UUID.
191
192 Args:
193 uuid (str): Unique identifier of the account.
194 edit_parameter (int): Parameter indicating which field to edit.
195 Valid values are 1 for application name, 2 for username,
196 3 for password, and 4 for URL.
197 """
107198 if edit_parameter == 1:
108199 field_name = 'application'
109200 new_value = input('Please enter your desired Application name: ')
@@ -113,10 +204,10 @@ def edit_account(uuid: str, edit_parameter: int) -> None:
113204 elif edit_parameter == 3:
114205 field_name = 'password'
115206 type_check = input(
116 'Do you want a new random password or to enter a custom password? (random/custom): ')
207 'Do you want a new random password or to enter a custom password? '
208 '(random/custom): ').lower()
117209 if type_check == 'random':
118 password_length = int(
119 input('Please enter your desired password length: '))
210 password_length = int(input('Please enter your desired password length: '))
120211 if password_length < 8:
121212 print('Error: Your password length must be at least 8 characters.')
122213 else: