krz/yoshi-cli

Archived

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

Commit c8617ce8ae

c8617ce8aecb40ccf1d13c02dcd7d7e74e79b56b

parent: 1fc982100a

Unregistered key

cmc <hello@cleberg.net> · 2025-04-05 18:36 UTC
committer: <noreply@github.com>

migrate from pylint to ruff-action (#4)

* migrate from pylint to ruff-action

* temporarily disable push param

* Commit from GitHub Actions (Pylint)

* re-enable push param

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>

Layout: unified · split

.github/workflows/pylint.yml +20 −6
@@ -1,23 +1,37 @@
11name: Pylint
22
3on: [push]
3on:
4 push:
5 paths:
6 - '**.py'
47
58jobs:
69 build:
710 runs-on: ubuntu-latest
811 strategy:
912 matrix:
10 python-version: ["3.8", "3.9", "3.10"]
13 python-version: ["3.x"]
1114 steps:
1215 - uses: actions/checkout@v4
1316 - name: Set up Python ${{ matrix.python-version }}
14 uses: actions/setup-python@v4
17 uses: actions/setup-python@v5
1518 with:
1619 python-version: ${{ matrix.python-version }}
20 ref: ${{ github.event.pull_request.head.ref }}
1721 - name: Install dependencies
1822 run: |
1923 python -m pip install --upgrade pip
20 pip install pylint cryptography prettytable
21 - name: Analysing the code with pylint
24 pip install cryptography prettytable
25 - name: Install Ruff
26 uses: astral-sh/ruff-action@v3.2.2
27 - name: Ruff Actions
2228 run: |
23 pylint -d R0801 $(git ls-files '*.py')
29 ruff check --fix
30 ruff format
31 - name: Add and Commit
32 uses: EndBug/add-and-commit@v9
33 env:
34 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 with:
36 default_author: github_actions
37 pathspec_error_handling: ignore
\ No newline at end of file
yoshi/__main__.py +1
@@ -4,4 +4,5 @@ Entry point for the yoshi cli app.
44
55if __name__ == "__main__":
66 from yoshi.cli import yoshi
7
78 yoshi()
yoshi/account.py +15 −9
@@ -11,9 +11,14 @@ from yoshi import database
1111class Account:
1212 """Represents a login account."""
1313
14 def __init__(self, uuid: str, application: str, #pylint: disable=R0913,R0917
15 username: str, #pylint: disable=R0913,R0917
16 password: str, url: str) -> None: #pylint: disable=R0913,R0917
14 def __init__(
15 self,
16 uuid: str,
17 application: str, # pylint: disable=R0913,R0917
18 username: str, # pylint: disable=R0913,R0917
19 password: str,
20 url: str,
21 ) -> None: # pylint: disable=R0913,R0917
1722 self.uuid = uuid
1823 self.application = application
1924 self.username = username
@@ -22,16 +27,17 @@ class Account:
2227
2328 def display_account(self) -> None:
2429 """Print the account details."""
25 print('ID:', self.uuid)
26 print('Application:', self.application)
27 print('Username:', self.username)
28 print('Password:', self.password)
29 print('URL:', self.url)
30 print("ID:", self.uuid)
31 print("Application:", self.application)
32 print("Username:", self.username)
33 print("Password:", self.password)
34 print("URL:", self.url)
3035
3136 def save_account(self) -> None:
3237 """Save the account details to the database."""
3338 database.add_account(
34 self.uuid, self.application, self.username, self.password, self.url)
39 self.uuid, self.application, self.username, self.password, self.url
40 )
3541
3642 def delete_account(self) -> bool:
3743 """Delete the account from the database.
yoshi/cli.py +38 −52
@@ -7,91 +7,76 @@ It imports the required modules and sets up a parser with basic options for demo
77import argparse
88from yoshi import crypto, database, process
99
10
1011def yoshi():
1112 """
1213 CLI entry point logic, used to parse user inputs.
1314 """
1415 parser = argparse.ArgumentParser(
15 description='Manage your username and passwords via a convenient CLI vault.'
16 description="Manage your username and passwords via a convenient CLI vault."
1617 )
1718
1819 # Top-level arguments
1920 group_one = parser.add_mutually_exclusive_group()
2021 group_one.add_argument(
21 '-n', '--new',
22 help='Create a new account.',
23 action='store_true'
22 "-n", "--new", help="Create a new account.", action="store_true"
2423 )
2524 group_one.add_argument(
26 '-l', '--list',
27 help='List all saved accounts.',
28 action='store_true'
25 "-l", "--list", help="List all saved accounts.", action="store_true"
2926 )
3027 group_one.add_argument(
31 '-e', '--edit',
32 help='Edit a saved account.',
33 action='store_true'
28 "-e", "--edit", help="Edit a saved account.", action="store_true"
3429 )
3530 group_one.add_argument(
36 '-d', '--delete',
37 help='Delete a saved account.',
38 action='store_true'
31 "-d", "--delete", help="Delete a saved account.", action="store_true"
3932 )
4033 group_one.add_argument(
41 '--purge',
34 "--purge",
4235 help=(
43 'Purge all accounts and delete the vault. '
44 '(Caution: this will irreversibly destroy your data.)'
36 "Purge all accounts and delete the vault. "
37 "(Caution: this will irreversibly destroy your data.)"
4538 ),
46 action='store_true'
47 )
48 group_one.add_argument(
49 '--encrypt',
50 help='Encrypt the vault.',
51 action='store_true'
52 )
53 group_one.add_argument(
54 '--decrypt',
55 help='Decrypt the vault.',
56 action='store_true'
39 action="store_true",
5740 )
41 group_one.add_argument("--encrypt", help="Encrypt the vault.", action="store_true")
42 group_one.add_argument("--decrypt", help="Decrypt the vault.", action="store_true")
5843
5944 # Encryption flags
6045 group_two = parser.add_mutually_exclusive_group()
6146 group_two.add_argument(
62 '-g', '--generate',
63 help=(
64 'When using the --encrypt option, generate a new encryption key.'
65 ),
66 action='store_true'
47 "-g",
48 "--generate",
49 help=("When using the --encrypt option, generate a new encryption key."),
50 action="store_true",
6751 )
6852 group_two.add_argument(
69 '-k', '--keyfile',
70 help='Path to existing key file.',
71 action='store',
53 "-k",
54 "--keyfile",
55 help="Path to existing key file.",
56 action="store",
7257 nargs=1,
73 type=str
58 type=str,
7459 )
7560
7661 # Edit flags
7762 group_three = parser.add_argument_group()
7863 group_three.add_argument(
79 '-u', '--uuid',
80 help=(
81 'When using the --edit or --delete options, provide the account UUID.'
82 ),
83 action='store',
64 "-u",
65 "--uuid",
66 help=("When using the --edit or --delete options, provide the account UUID."),
67 action="store",
8468 nargs=1,
85 type=str
69 type=str,
8670 )
8771 group_three.add_argument(
88 '-f', '--field',
72 "-f",
73 "--field",
8974 help=(
90 'When using the --edit option, specify the field to edit (integer index).'
75 "When using the --edit option, specify the field to edit (integer index)."
9176 ),
92 action='store',
77 action="store",
9378 nargs=1,
94 type=int
79 type=int,
9580 )
9681
9782 args = parser.parse_args()
@@ -100,22 +85,22 @@ def yoshi():
10085 if args.keyfile:
10186 key = crypto.load_key(args.keyfile[0])
10287 else:
103 key = input('Please enter your decryption key: ')
88 key = input("Please enter your decryption key: ")
10489 crypto.decrypt(key)
10590 elif args.encrypt:
10691 if args.generate:
10792 key = crypto.generate_key()
10893 print(
109 'WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT '
110 'YOUR DATA WITHOUT IT!'
94 "WRITE THIS KEY DOWN SOMEWHERE SAFE. YOU WILL NOT BE ABLE TO DECRYPT "
95 "YOUR DATA WITHOUT IT!"
11196 )
11297 print(key.decode())
113 print('\n')
98 print("\n")
11499 else:
115100 if args.keyfile:
116101 key = crypto.load_key(args.keyfile[0])
117102 else:
118 key = input('Please enter your encryption key: ')
103 key = input("Please enter your encryption key: ")
119104 crypto.encrypt(key)
120105 elif database.check_table():
121106 if args.new:
@@ -130,8 +115,9 @@ def yoshi():
130115 process.purge_accounts()
131116 else:
132117 raise TypeError(
133 'Please specify a command or use the --help flag for more information.'
118 "Please specify a command or use the --help flag for more information."
134119 )
135120
121
136122if __name__ == "__main__":
137123 yoshi()
yoshi/crypto.py +6 −6
@@ -6,7 +6,7 @@ It allows for secure encryption and decryption of data using a secret key.
66
77from cryptography.fernet import Fernet
88
9VAULT_FILE = 'vault.sqlite'
9VAULT_FILE = "vault.sqlite"
1010
1111
1212def generate_key() -> bytes:
@@ -21,25 +21,25 @@ def load_key(key_file: str) -> bytes:
2121 Args:
2222 key_file (str): Path to the key file.
2323 """
24 with open(key_file, 'rb') as key:
24 with open(key_file, "rb") as key:
2525 return key.read()
2626
2727
2828def encrypt(key: bytes, filename: str = VAULT_FILE) -> None:
2929 """Encrypts the data in the specified file using the provided key."""
3030 f = Fernet(key)
31 with open(filename, 'rb') as vault:
31 with open(filename, "rb") as vault:
3232 data = vault.read()
3333 encrypted_data = f.encrypt(data)
34 with open(filename, 'wb') as vault:
34 with open(filename, "wb") as vault:
3535 vault.write(encrypted_data)
3636
3737
3838def decrypt(key: bytes, filename: str = VAULT_FILE) -> None:
3939 """Decrypts the data in the specified file using the provided key."""
4040 f = Fernet(key)
41 with open(filename, 'rb') as vault:
41 with open(filename, "rb") as vault:
4242 encrypted_data = vault.read()
4343 decrypted_data = f.decrypt(encrypted_data)
44 with open(filename, 'wb') as vault:
44 with open(filename, "wb") as vault:
4545 vault.write(decrypted_data)
yoshi/database.py +31 −29
@@ -7,8 +7,8 @@ import sqlite3
77import sys
88import os
99
10VAULT_DECRYPTED = 'vault.sqlite'
11VAULT_ENCRYPTED = 'vault.sqlite.aes'
10VAULT_DECRYPTED = "vault.sqlite"
11VAULT_ENCRYPTED = "vault.sqlite.aes"
1212
1313
1414def create_table() -> None:
@@ -16,8 +16,8 @@ def create_table() -> None:
1616 db_connection = sqlite3.connect(VAULT_DECRYPTED)
1717 cursor = db_connection.cursor()
1818 cursor.execute(
19 ''' CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
20 username text, password text, url text) '''
19 """ CREATE TABLE IF NOT EXISTS accounts (uuid text, application text,
20 username text, password text, url text) """
2121 )
2222 db_connection.commit()
2323 db_connection.close()
@@ -29,17 +29,18 @@ def check_table() -> bool:
2929 db_connection = sqlite3.connect(VAULT_DECRYPTED)
3030 cursor = db_connection.cursor()
3131 cursor.execute(
32 ''' SELECT count(name) FROM sqlite_master WHERE type='table'
33 AND name='accounts' '''
32 """ SELECT count(name) FROM sqlite_master WHERE type='table'
33 AND name='accounts' """
3434 )
3535 if cursor.fetchone()[0] != 1:
3636 user_choice = input(
37 'Password vault does not exist. Would you like to create it now? (y/n): ')
38 if user_choice.lower() == 'y':
37 "Password vault does not exist. Would you like to create it now? (y/n): "
38 )
39 if user_choice.lower() == "y":
3940 create_table()
4041 check = True
4142 else:
42 sys.exit('Program aborted upon user request.')
43 sys.exit("Program aborted upon user request.")
4344 else:
4445 check = True
4546 db_connection.commit()
@@ -47,17 +48,22 @@ def check_table() -> bool:
4748 return check
4849
4950
50def add_account(uuid: str, application: str, username: str, password: str,
51 url: str) -> None:
51def add_account(
52 uuid: str, application: str, username: str, password: str, url: str
53) -> None:
5254 """Add a new account within the vault database."""
5355 db_connection = sqlite3.connect(VAULT_DECRYPTED)
5456 cursor = db_connection.cursor()
5557 cursor.execute(
56 ''' INSERT INTO accounts VALUES (:uuid,:application,:username,
57 :password,:url) ''', {
58 'uuid': uuid, 'application': application, 'username': username,
59 'password': password, 'url': url
60 }
58 """ INSERT INTO accounts VALUES (:uuid,:application,:username,
59 :password,:url) """,
60 {
61 "uuid": uuid,
62 "application": application,
63 "username": username,
64 "password": password,
65 "url": url,
66 },
6167 )
6268 db_connection.commit()
6369 db_connection.close()
@@ -67,9 +73,7 @@ def delete_account(uuid: str) -> None:
6773 """Delete an account within the vault database by its unique ID."""
6874 db_connection = sqlite3.connect(VAULT_DECRYPTED)
6975 cursor = db_connection.cursor()
70 cursor.execute(
71 ''' DELETE FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
72 )
76 cursor.execute(""" DELETE FROM accounts WHERE uuid = :uuid """, {"uuid": uuid})
7377 db_connection.commit()
7478 db_connection.close()
7579
@@ -78,9 +82,7 @@ def find_account(uuid: str) -> list:
7882 """Find an account within the vault database by its unique ID."""
7983 db_connection = sqlite3.connect(VAULT_DECRYPTED)
8084 cursor = db_connection.cursor()
81 cursor.execute(
82 ''' SELECT * FROM accounts WHERE uuid = :uuid ''', {'uuid': uuid}
83 )
85 cursor.execute(""" SELECT * FROM accounts WHERE uuid = :uuid """, {"uuid": uuid})
8486 account = cursor.fetchall()
8587 db_connection.close()
8688 return account
@@ -90,7 +92,7 @@ def find_accounts() -> list:
9092 """Return all accounts stored within the vault database."""
9193 db_connection = sqlite3.connect(VAULT_DECRYPTED)
9294 cursor = db_connection.cursor()
93 cursor.execute(''' SELECT * FROM accounts ''')
95 cursor.execute(""" SELECT * FROM accounts """)
9496 accounts = cursor.fetchall()
9597 db_connection.close()
9698 return accounts
@@ -99,14 +101,14 @@ def find_accounts() -> list:
99101def update_account(field_name: str, new_value: str, uuid: str) -> None:
100102 """Update an account within the vault database by its unique ID."""
101103 queries = {
102 'application': 'UPDATE accounts SET application = :new_value WHERE uuid = :uuid',
103 'username': 'UPDATE accounts SET username = :new_value WHERE uuid = :uuid',
104 'password': 'UPDATE accounts SET password = :new_value WHERE uuid = :uuid',
105 'url': 'UPDATE accounts SET url = :new_value WHERE uuid = :uuid'
104 "application": "UPDATE accounts SET application = :new_value WHERE uuid = :uuid",
105 "username": "UPDATE accounts SET username = :new_value WHERE uuid = :uuid",
106 "password": "UPDATE accounts SET password = :new_value WHERE uuid = :uuid",
107 "url": "UPDATE accounts SET url = :new_value WHERE uuid = :uuid",
106108 }
107109 db_connection = sqlite3.connect(VAULT_DECRYPTED)
108110 cursor = db_connection.cursor()
109 cursor.execute(queries[field_name], {'new_value': new_value, 'uuid': uuid})
111 cursor.execute(queries[field_name], {"new_value": new_value, "uuid": uuid})
110112 db_connection.commit()
111113 db_connection.close()
112114
@@ -115,7 +117,7 @@ def purge_table() -> None:
115117 """Purge the 'accounts' table within the vault database."""
116118 db_connection = sqlite3.connect(VAULT_DECRYPTED)
117119 cursor = db_connection.cursor()
118 cursor.execute(''' DROP TABLE accounts ''')
120 cursor.execute(""" DROP TABLE accounts """)
119121 db_connection.commit()
120122 db_connection.close()
121123
yoshi/process.py +58 −47
@@ -32,6 +32,7 @@ from yoshi.account import Account
3232from yoshi import database
3333from yoshi.wordlist import WORDLIST
3434
35
3536def generate_characters(n: int) -> list:
3637 """
3738 Generates a list of n random characters from the set of ASCII letters,
@@ -61,7 +62,7 @@ def shuffle_characters(characters: list) -> str:
6162 str: A string representation of the shuffled characters
6263 """
6364 random.shuffle(characters)
64 character_string = ''.join(characters)
65 character_string = "".join(characters)
6566 return character_string
6667
6768
@@ -96,7 +97,7 @@ def list_accounts() -> None:
9697 None
9798 """
9899 accounts = database.find_accounts()
99 t = PrettyTable(['UUID', 'Application', 'Username', 'Password', 'URL'])
100 t = PrettyTable(["UUID", "Application", "Username", "Password", "URL"])
100101 for account in accounts:
101102 t.add_row([account[0], account[1], account[2], account[3], account[4]])
102103 print(t)
@@ -113,13 +114,15 @@ def delete_account(account_uuid: str) -> None:
113114 None
114115 """
115116 account_record = database.find_account(account_uuid)
116 account = Account(account_record[0][0],
117 account_record[0][1],
118 account_record[0][2],
119 account_record[0][3],
120 account_record[0][4])
117 account = Account(
118 account_record[0][0],
119 account_record[0][1],
120 account_record[0][2],
121 account_record[0][3],
122 account_record[0][4],
123 )
121124 if account.delete_account():
122 print('Account successfully deleted.')
125 print("Account successfully deleted.")
123126
124127
125128def purge_accounts() -> None:
@@ -130,12 +133,15 @@ def purge_accounts() -> None:
130133 None
131134 """
132135 check = input(
133 '''Are you absolutely sure you want to delete your password vault?
134 This action is irreversible. (y/n): ''')
135 if check.lower() == 'y':
136 """Are you absolutely sure you want to delete your password vault?
137 This action is irreversible. (y/n): """
138 )
139 if check.lower() == "y":
136140 database.purge_table()
137141 database.purge_database()
138 print('The password vault has been purged. You may now exit or create a new one.')
142 print(
143 "The password vault has been purged. You may now exit or create a new one."
144 )
139145
140146
141147def create_account() -> None:
@@ -145,46 +151,50 @@ def create_account() -> None:
145151 Returns:
146152 None
147153 """
148 application_string = input('Please enter a name for this account: ')
149 username_string = input('Please enter your username for this account: ')
150 url_string = input('(Optional) Please enter a URL for this account: ')
154 application_string = input("Please enter a name for this account: ")
155 username_string = input("Please enter your username for this account: ")
156 url_string = input("(Optional) Please enter a URL for this account: ")
151157
152158 password_type = input(
153 '''Do you want a random character password (p), an XKCD-style passphrase
154(x), or a custom password (c)? (p|x|c): '''
159 """Do you want a random character password (p), an XKCD-style passphrase
160(x), or a custom password (c)? (p|x|c): """
155161 )
156 if password_type not in ['p', 'x', 'c']:
157 print('Error: Invalid choice. Please choose p, x, or c.')
162 if password_type not in ["p", "x", "c"]:
163 print("Error: Invalid choice. Please choose p, x, or c.")
158164 return
159165
160 if password_type == 'x':
166 if password_type == "x":
161167 password_length = int(
162 input('Please enter number of words to include (min. 2): ')
168 input("Please enter number of words to include (min. 2): ")
163169 )
164170 if password_length < 3:
165 print('Error: Your passphrase length must be at least 3 words.')
171 print("Error: Your passphrase length must be at least 3 words.")
166172 return
167173 password_separator = input(
168 'Please enter your desired separator symbol (_,-, ~, etc.): '
174 "Please enter your desired separator symbol (_,-, ~, etc.): "
169175 )
170176 password_string = generate_passphrase(password_length, password_separator)
171 elif password_type == 'p':
177 elif password_type == "p":
172178 password_length = int(
173 input('Please enter your desired password length (min. 8): ')
179 input("Please enter your desired password length (min. 8): ")
174180 )
175181 if password_length < 8:
176 print('Error: Your password length must be at least 8 characters.')
182 print("Error: Your password length must be at least 8 characters.")
177183 return
178184 password_characters = generate_characters(password_length)
179185 password_string = shuffle_characters(password_characters)
180186 else:
181 password_string = input('Please enter your desired password: ')
182
183 account = Account(str(uuid.uuid4()), application_string,
184 username_string, password_string, url_string)
187 password_string = input("Please enter your desired password: ")
188
189 account = Account(
190 str(uuid.uuid4()),
191 application_string,
192 username_string,
193 password_string,
194 url_string,
195 )
185196 account.save_account()
186 print('Account saved to the vault. Use `--list` to see all saved accounts.')
187
197 print("Account saved to the vault. Use `--list` to see all saved accounts.")
188198
189199
190200def edit_account(account_uuid: str, edit_parameter: int) -> None:
@@ -197,29 +207,30 @@ def edit_account(account_uuid: str, edit_parameter: int) -> None:
197207 Valid values are 1 for application name, 2 for username,
198208 3 for password, and 4 for URL.
199209 """
200 field_name, new_value = ''
210 field_name, new_value = ""
201211 if edit_parameter == 1:
202 field_name = 'application'
203 new_value = input('Please enter your desired Application name: ')
212 field_name = "application"
213 new_value = input("Please enter your desired Application name: ")
204214 elif edit_parameter == 2:
205 field_name = 'username'
206 new_value = input('Please enter your desired username: ')
215 field_name = "username"
216 new_value = input("Please enter your desired username: ")
207217 elif edit_parameter == 3:
208 field_name = 'password'
218 field_name = "password"
209219 type_check = input(
210 'Do you want a new random password or to enter a custom password? '
211 '(random/custom): ').lower()
212 if type_check == 'random':
213 password_length = int(input('Please enter your desired password length: '))
220 "Do you want a new random password or to enter a custom password? "
221 "(random/custom): "
222 ).lower()
223 if type_check == "random":
224 password_length = int(input("Please enter your desired password length: "))
214225 if password_length < 8:
215 print('Error: Your password length must be at least 8 characters.')
226 print("Error: Your password length must be at least 8 characters.")
216227 else:
217228 password_characters = generate_characters(password_length)
218229 new_value = shuffle_characters(password_characters)
219230 else:
220 new_value = input('Please enter your desired password: ')
231 new_value = input("Please enter your desired password: ")
221232 elif edit_parameter == 4:
222 field_name = 'url'
223 new_value = input('Please enter your desired URL: ')
233 field_name = "url"
234 new_value = input("Please enter your desired URL: ")
224235 database.update_account(field_name, new_value, account_uuid)
225 print('Account successfully updated.')
236 print("Account successfully updated.")