krz/yoshi-cli

A password manager for the command line.

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

main: yoshi/process.py · raw

  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 string import ascii_letters, punctuation, digits
 27import random
 28import secrets
 29import uuid
 30from prettytable import PrettyTable
 31from yoshi.account import Account
 32from yoshi import database
 33from yoshi.wordlist import WORDLIST
 34
 35
 36def generate_characters(n: int) -> list:
 37    """
 38    Generates a list of n random characters from the set of ASCII letters,
 39    punctuation and digits.
 40
 41    Args:
 42        n (int): The number of characters to generate
 43
 44    Returns:
 45        list: A list of n random characters
 46    """
 47    characters = []
 48    password_format = ascii_letters + punctuation + digits
 49    for _ in range(n):
 50        characters.append(secrets.choice(password_format))
 51    return characters
 52
 53
 54def shuffle_characters(characters: list) -> str:
 55    """
 56    Shuffles the characters to create a password.
 57
 58    Args:
 59        characters (list): The list of characters
 60
 61    Returns:
 62        str: A string representation of the shuffled characters
 63    """
 64    random.shuffle(characters)
 65    character_string = "".join(characters)
 66    return character_string
 67
 68
 69def generate_passphrase(n: int, sep: str) -> str:
 70    """
 71    Generates an XKCD-style passphrase with n words and separator.
 72
 73    Args:
 74        n (int): The number of words to include
 75        sep (str): The separator symbol
 76
 77    Returns:
 78        str: A string representation of the passphrase
 79    """
 80    phrases = []
 81    lucky_number = secrets.choice(range(0, n))
 82    for _ in range(n):
 83        word = secrets.choice(WORDLIST)
 84        if _ == lucky_number:
 85            phrases.append(word.capitalize() + str(_))
 86        else:
 87            phrases.append(word.capitalize())
 88    passphrase = sep.join(phrases)
 89    return passphrase
 90
 91
 92def list_accounts() -> None:
 93    """
 94    Lists all saved accounts in the database.
 95
 96    Returns:
 97        None
 98    """
 99    accounts = database.find_accounts()
100    t = PrettyTable(["UUID", "Application", "Username", "Password", "URL"])
101    for account in accounts:
102        t.add_row([account[0], account[1], account[2], account[3], account[4]])
103    print(t)
104
105
106def delete_account(account_uuid: str) -> None:
107    """
108    Deletes an account by its UUID.
109
110    Args:
111        account_uuid (str): The UUID of the account to delete
112
113    Returns:
114        None
115    """
116    account_record = database.find_account(account_uuid)
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    )
124    if account.delete_account():
125        print("Account successfully deleted.")
126
127
128def purge_accounts() -> None:
129    """
130    Purges the entire database (irreversible).
131
132    Returns:
133        None
134    """
135    check = input(
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":
140        database.purge_table()
141        database.purge_database()
142        print(
143            "The password vault has been purged. You may now exit or create a new one."
144        )
145
146
147def create_account() -> None:
148    """
149    Creates a new account by prompting user for details.
150
151    Returns:
152        None
153    """
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: ")
157
158    password_type = input(
159        """Do you want a random character password (p), an XKCD-style passphrase
160(x), or a custom password (c)? (p|x|c): """
161    )
162    if password_type not in ["p", "x", "c"]:
163        print("Error: Invalid choice. Please choose p, x, or c.")
164        return
165
166    if password_type == "x":
167        password_length = int(
168            input("Please enter number of words to include (min. 2): ")
169        )
170        if password_length < 3:
171            print("Error: Your passphrase length must be at least 3 words.")
172            return
173        password_separator = input(
174            "Please enter your desired separator symbol (_,-, ~, etc.): "
175        )
176        password_string = generate_passphrase(password_length, password_separator)
177    elif password_type == "p":
178        password_length = int(
179            input("Please enter your desired password length (min. 8): ")
180        )
181        if password_length < 8:
182            print("Error: Your password length must be at least 8 characters.")
183            return
184        password_characters = generate_characters(password_length)
185        password_string = shuffle_characters(password_characters)
186    else:
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    )
196    account.save_account()
197    print("Account saved to the vault. Use `--list` to see all saved accounts.")
198
199
200def edit_account(account_uuid: str, edit_parameter: int) -> None:
201    """
202    Allow users to edit any account information except the UUID.
203
204    Args:
205        account_uuid (str): Unique identifier of the account.
206        edit_parameter (int): Parameter indicating which field to edit.
207            Valid values are 1 for application name, 2 for username,
208            3 for password, and 4 for URL.
209    """
210    field_name, new_value = ""
211    if edit_parameter == 1:
212        field_name = "application"
213        new_value = input("Please enter your desired Application name: ")
214    elif edit_parameter == 2:
215        field_name = "username"
216        new_value = input("Please enter your desired username: ")
217    elif edit_parameter == 3:
218        field_name = "password"
219        type_check = input(
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: "))
225            if password_length < 8:
226                print("Error: Your password length must be at least 8 characters.")
227            else:
228                password_characters = generate_characters(password_length)
229                new_value = shuffle_characters(password_characters)
230        else:
231            new_value = input("Please enter your desired password: ")
232    elif edit_parameter == 4:
233        field_name = "url"
234        new_value = input("Please enter your desired URL: ")
235    database.update_account(field_name, new_value, account_uuid)
236    print("Account successfully updated.")