Questo è uno dei primi progetti interessanti che ho scritto in Python, dopo aver imparato le basi del linguaggio. Un semplicissimo password manager, basato sulla libreria cryptography e la classe Fernet, per renderlo minimale non utilizzerò classi, ma solo funzioni per definirne ogni aspetto, dalla creazione delle chiavi al file criptato che conterrà le password. Non è necessario conoscere nei minimi particolari la libreria e il suo funzionamento, ma una lettura alla Documentazione tanto da capire gli elementi che la compongono e come funzionano, vi consiglio di darla, anche perché è una libreria molto vasta che contiene molto altro, altamente supportata e utile per altri progetti che necessitano di criptografia simmetrica. Detto ciò bando alle ciance e vediamo com'è possibile implementarsi il proprio password manager in autonomia.
Premetto che userò e scriverò il codice su Linux, che è il sistema che uso quotidianamente. Ma potete seguire questa guida tranquillamente anche su Windows, specie se utilizzate Visual Studio Code.
Il setup su Windows è relativamente semplice vi basta scaricare Python, e Visual Studio Code per semplificarvi un po' la vita. Per il setup su Linux non serve molto, Python è già installato di default sulla maggior parte delle distro, l'unica cosa che probabilmente vi serve sono i pacchetti dev di Python che potete installare molto facilmente con il vostro package manager di sistema. Sotto vi lascio qualche esempio per Debian e Arch.
sudo apt update
sudo apt install python3-dev python3-venv
sudo pacman -Syu
sudo pacman -S python3-dev python3-venv
Se volete installare anche Visual Studio Code, vi basta andare su sito di VSC, scaricare il pacchetto per la vostra distro e il resto non è diverso dal'installazione di qualsiasi altro pacchetto, ma se volete che vi prepari una guida anche per il setup di VSC, sia per Linux che per Windows, vi basta scrivermi ai miei contatti.
Come in ogni mio progetto Python creo sempre un virtual environment (abbreviato in venv). Un venv è un ambiente di sviluppo virtuale che permette di mantenere le dipendenze dei nostri progetti racchiuse in esso, e separate dagli altri. Funzionano alla stessa maniera sia su Windows che su Linux.
Per creare un venv ci basta chiamare python con l'opzione -m venv seguito dal nome per l'ambiente, in genere semplicemente venv, ma nulla vi vieta di chiamarlo come volete.
python -m venv venv
python3 -m venv venv
Dopo aver creato l'ambiente bisogna caricarlo, in Linux è presente il comando source che ci semplifica la vita, su Windows e leggermente diverso, innanzitutto dovrete attivarel'ambiente eseguendo lo script presente nella cartella venv\Scripts\activate.bat, e se usate VSC selezionarlo come sotto.
source ./venv/bin/activate
Come già accennato per questo progetto utilizzerò la libreria cryptography, che è un modulo Python esterno, per installarlo nell'ambiente che abbiamo creato ci basta usare pip, come sotto.
.\venv\Scripts\pip install cryptography
venv/bin/pip install cryptography
Per non avere dubbi sulla corretta installazione di cryptography, possiamo fare questo piccolo test preso dalla Documentazione ufficiale.
from cryptography.fernet import Fernet secret_message = b"hello" key = Fernet.generate_key() c_message = Fernet(key).encrypt(secret_message) print(f"Message: {secret_message.decode()}") print(f"Encrypted message: {c_message.decode()}")
Come output dovrebbe restituire un risultato del genere:
Message: hello
Encrypted message: gAAAAABpR_F0hY9zPelWo8N8hipaSAvB89l0D...
Se vi dà errore, controllate la cartella venv/lib/python.../site-packages che deve contenere:
Inoltre tenete presente che, come riportato nella documentazione ufficiale (dove trovate ulteriori informazioni sulla compatibilità di tale libreria), cryptography è disponibile da Python 3.8+, se avete una versione più vecchia purtroppo non è possibile usare tale libreria.
La prima funzione che definisco è create_key(), che serve a creare nuove chiavi con cui cifrare e decifrare, i dati delle password che vogliamo salvare.
from cryptography.fernet import Fernet def create_key(path): with open(path, 'wb') as f: key = Fernet.generate_key() f.write(key)
La funzione prende un solo parametro (path), che sarà il nome del file che conterrà tale chiave. Ora definisco il main menu dove vengono chiamate tutte le funzioni del programma.
from cryptography.fernet import Fernet def create_key(path): ... def main(): while True: print("""Menu: 1. Create a new key. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": name_key = input("Enter a name for the new key: ") create_key(name_key) print("Key generated.") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Il menu è molto basilare, se avete già programmatto in Python o in altri linguaggi, non è sicuramente qualcosa di nuovo. Si definisce un ciclo infinito che stampa le operazioni che può svolgere il programma, una variabile che prende in input l'opzione, e si gestisce il tutto con blocchi if, elif ed else, e in fine si chiama il menu per far partire il tutto.
Una volta creata una chiave bisogna anche far si che possa essere caricata nel programma, quindi definisco una funzione che fa proprio ciò.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): with open(path, 'rb') as f: key = f.read() return key def main(): ... main()
Questa funzione però potrebbe anche fallire, se l'utente sbaglia ad'inserire il nome della chiave, o se non è presente nella working directory dove risiede il programma. Questa eccezione si chiama FileNotFoundError, che, da gestire in Python, è molto semplice, bastano due blocchi try ed except (prova-eccezione). Il funzionamento è molto facile da comprendere, nel blocco try inseriamo il codice che potrebbe dare problemi, e bloccare il programma, nel blocco except l'eccezione con un eventuale messaggio di errore, per l'utente, in questo modo il programma può continuare l'eseguzione anche in caso di errori.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): try: with open(path, 'rb') as f: key = f.read() return key except FileNotFoundError: print(f"Key -{path}- not found!") def main(): ... main()
Dopodiché aggiorno la funzione main, aggiungendo la variabile locale key inizializzandola a None, la nuova opzione per caricare la chiave esistente e in fine modifico l'opzione precedente, per caricare automaticamente la chiave generata.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def main(): key = None while True: print("""Menu: 1. Create a new key. 2. Load an existing key. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": name_key = input("Enter a name for the new key: ") create_key(name_key) key = load_key(name_key) print("Key generated and loaded.") elif choice == "2": name_key = input("Enter the name of an existing key: ") key = load_key(name_key) print("Key loaded successfully.") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Ora che é possibile creare e caricare le chiavi per criptare e decriptare i dati, posso definire la logica per generare il password file. Per tale fine ho scelto di utilizzare i dizionari Python, che funzionano come le liste, con la differenza che ad ogni voce è associato un valore (dict = { 'key': value }). Per ottenere un risultato del genere:
email = { 'indirizzo': password }
instagram = { 'username': password }
Netflix = { 'username': password }
Pass-x = { 'None': password }
Il nome della dict sarà l'identificativo del servizio con, al suo interno, un unica chiave che potrà essere anche vuota (in quel caso sarà None), e il valore associato sarà l'effetiva password.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def create_new_pass_file(path, key, pass_dict, service, username, password): fernet = Fernet(key) service = fernet.encrypt(service.encode()).decode() username = fernet.encrypt(username.encode()).decode() password = fernet.encrypt(password.encode()).decode() pass_dict[service] = {username: password} with open(path, 'w') as f: for dict_key, dict_value in pass_dict.items(): f.write("[" + dict_key + "]\n") for Ikey, Ivalue in dict_value.items(): f.write(Ikey + ":" + Ivalue + "\n") def main(): ... main()
La funzione prende un certo numero di argomenti (path, key, pass_dict, service, username, password), path come in precedenza serve a dare il nome stesso al password file. key è la chiave che serve per la crittografia di tutti i campi del password file, prima di scriverli all'interno dello stesso. pass_dict è il dizionario interno al programma dove carico le dict cifrate dal password file. Mentre service, username e password i campi del password file stesso.
Come primo step creo l'oggetto Fernet per crittografare service, username e password, inserendo il tutto dentro la dict interna pass_dict. Sicomme volevo mantenere il programma più minimale possibile, senza utilizzare json, YAML, pickle o CSV ho scelto di usare un ciclo for per iterare con due parametri (dict_key e dict_value) sulla dict all'interno di pass_dict, per poi scrivere la chiave racchiusa tra parentesi '[]' come prima riga che mi servirà come riferimento successivamente. Con il for annidato itero con altri due parametri (Ikey e Ivalue, che stanno per internal key e value) per identificare lo username e password per scriverli nella riga subito sotto separati dal carattere ':'. Non resta che aggiornare il main menu.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def main(): key = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": if key: pass_file = input("Enter a name for the new password file: ") service = input("Enter the service name: ") username = input("Enter the username: ") password = input("Enter the password: ") create_new_pass_file(pass_file, key, pass_dict, service, username, password) print("Password file generated successfully!") else: print("Please generate or load a key first!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Prima di tutto inizializzo la pass_dict, che dovrà per forza di cose essere vuota. Questa e altre opzioni del programma necessitano che ci sia una chiave già caricata, quindi come prima cosa verifico che la chiave esista. Siccome la chiave di base è inizializzata a None (valore nullo), la condizione if key risultera vera solo se la chiave è stata effettivamente caricata. Questo è un approcio molto comune in questi casi per verificare se una data variabile esista, ovvero possegga un valore, sia in Python che in altri linguaggi.
Come per la chiave, anche il password file dev'essere caricato nel programma per decifrare e leggere i servizi al suo interno. Per fare ciò serve una funzione che legga il file riga per riga, e carichi le dict correttamente nella pass_dict.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def load_pass_file(path, pass_dict): try: with open(path, 'r') as f: for line in f: line = line.strip() if not line: continue if line.startswith('[') and line.endswith(']'): current_key = line[1:-1] pass_dict[current_key] = {} elif ':' in line and cur rent_key: key, value = line.split(':', 1) pass_dict[current_key][key] = value return pass_dict except FileNotFoundError: print("Password file not found!") def main(): ... main()
La funzione accetta solo due argomenti, path e pass_dict che ormai dovrebbe essere abbastanza chiaro che cosa rappresentano. Come per load_key() bisogna gestire l'eccezione FileNotFoundError nel caso l'utente inserisca un nome errato, subito dopo apre il path in 'r' (read mode), ed esegue un semplice ciclo for che legge riga per riga l'intero file. La prima condizione if verifica che non sia una riga vuota nel qual caso salta all'iterazione successiva con il costrutto continue, in seguito verifica se la riga inizia (.startswith) con il carattere '[' e finisce (.endswith) con ']', che, se ricordate, indica il nome del servizio, in fine crea una dict all'interno di pass_dict, tolgliendo la parte che non serve con un semplice string slicing dall'inizio della riga +1 alla fine -1 ([1:-1]). L'ultima condizione identifica la coppia 'username': password, con uno split, inserendoli nella dict creata con la precedente condizione.
Non resta che aggiornare il main menu aggiungendo una nuova variabile locale, che rappresenterà il password file (vedremo più avanti il perché) e l'opzione precedente.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def load_pass_file(path, pass_dict): ... def main(): key = None pass_file = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. 4. Load an existing password file. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": if key: pass_file = input("Enter the name for the new password file: ") service = input("Enter the service name: ") username = input("Enter the username: ") password = input("Enter the password: ") pass_dict.clear() create_new_pass_file(pass_file, key, pass_dict, service, username, password) load_pass_file(pass_file, pass_dict) print("Password file generated successfully!") else: print("Please generate or load a key first.") elif choice == "4": if key: pass_file = input("Enter the name of an existing password file: ") pass_dict.clear() load_pass_file(pass_file, pass_dict) print("Password file loaded successfully!") else: print("Please generate or load a valid key first!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Avendo ora una funzione che prende e carica dati dalla pass_dict, per evitare di utilizzare dati di altri file, è importante che questa sia vuota. Quindi utilizzo la funzione .clear() per pulire la dict, prima di eseguire tali operazioni.
Ora che il programma può generare e caricare le chiavi, e lo stesso con i password file, non è male poter aggiungere nuovi servizi ad essi. Ma prima definisco una funzione che aggiorni il password file attualmente caricato, con lo scopo di rendere quest'azione più automatica, e non dover far copia/incolla del vecchio codice, apportandogli solo qualche piccola modifica, ogni volta che serve aggiornare lo stesso.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): with open(path, mode) as f: for key, value in pass_dict.items(): f.write("[" + key + "]\n") for Ikey, Ivalue in value.items(): f.write(Ikey + ":" + Ivalue + "\n") def create_new_pass_file(path, key, pass_dict, service, username, password): ... def load_pass_file(path, pass_dict): ... def main(): ... main()
Come argomenti per forza di cose prende sia il path che pass_dict, e una modalità in cui aprire il file, per eseguire la modifica nel modo più opportuno. Ora bisogna solo aggiornare create_new_pass_file().
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): fernet = Fernet(key) service = fernet.encrypt(service.encode()).decode() username = fernet.encrypt(username.encode()).decode() password = fernet.encrypt(password.encode()).decode() pass_dict[service] = {username: password} update_pass_file(path, pass_dict, 'w') def main(): ... main()
Adesso definisco add_service().
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): fernet = Fernet(key) service = fernet.encrypt(service.encode()).decode() username = fernet.encrypt(username.encode()).decode() password = fernet.encrypt(password.encode()).decode() pass_dict[service] = {username: password}} update_pass_file(path, pass_dict, 'w') def main(): ... main()
La funzione prende gli stessi argomenti di create_new_pass_file() e svolge quasi le stesse operazioni, con la differenza che per l'update del password file utilizza la modalità 'a+' (append), che non sovrascrive l'intero file ma aggiunge, alla fine dello stesso, solo le rige del servizio aggiunto. Ora non resta che aggionare il main menu.
from cryptography.fernet import Fernet def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def main(): key = None pass_file = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. 4. Load an existing password file. 5. Add a new service. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": ... elif choice == "4": ... elif choice == "5": if key and pass_file: service = input("Enter the service name: ") username = input("Enter the username: ") password = input("Enter the password: ") add_service(pass_file, key, pass_dict, service, username, password) load_pass_file(pass_file, pass_dict) print("Service added!") else: print("Please generate or load a valid key and a password file!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Per questa opzione, non solo abbiamo non solo bisogna verificare che ci sia una chiave caricata, ma anche un password file, perché, com'è facilmente intuibile, add_service() lavora sulla pass_dict e sul password file, che se non è caricato può dare problemi. Quindi la condizione diventa doppia (if key and pass_file).
Un password file può contenere diversi servizi, ed è facile dimenticarsi quali abbiamo salvato in uno di essi, specialmente se non sono password che utiliziamo frequentemente. Per questo motivo definisco anche una funzione che produca una lista di tutti i servizi presenti nel file caricato. Ora, questa funzione utilizzerà decrypt() per decriptare i soli servizi, e mostrarne la lista, che è l'opposto di encrypt() utilizzato fin'ora per crittografare i dati del file. Tale funzione, se utilizza una chiave errata, può generare l'eccezione InvalidToken e bloccare il processo. Per cui bisogna gestirla, per non mandare in crash il programma.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): fernet = Fernet(key) print("Services:") for i in pass_dict: try: service = fernet.decrypt(i.encode()).decode() print(service) except InvalidToken: print("Invalid key for this password file!") return def main(): ... main()
Questa funzione prende come argomenti pass_dict e key, tramite un semplicissimo ciclo for che itera su pass_dict, decripta uno per uno i soli servizi contenuti in essa, per poi stamparli in output, sotto forma di lista.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def main(): key = None pass_file = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. 4. Load an existing password file. 5. Add a new service. 6. List all services in the current password file. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": ... elif choice == "4": ... elif choice == "5": ... elif choice == "6": if key and pass_file: list_service(pass_dict, key) else: print("Please generate or load a valid key and a password file!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Oltre ad una lista di tutti i servizi presenti nel file, ottenere le password decriptate in esso contenute è quella che definirei una delle funzioni principali del programma, se no risulterebbe abbastanza difficile recuperare le nostre password. La funzione get_service() permette proprio questo.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): fernet = Fernet(key) for service, usr_and_pass in pass_dict.items(): try: service = fernet.decrypt(service.encode()).decode() except InvalidToken: print("Invalid key for this password file") return if service == name_service: for username, password in usr_and_pass.items(): username = fernet.decrypt(username.encode()).decode() password = fernet.decrypt(password.encode()).decode() print(f"Service: {service}\nUsername: {username} | Passw ord: {password}") else: print("Service not found!") def main(): ... main()
La funzione prende in input il nome del servizio, ed esegue un ciclo for alla ricerca dello stesso. Se nel pass_dict esiste il servizio cercato, la password e lo username vengono decriptati e stampati in output. Se non viene trovato nessun servizio corrispondente viene stampato un avviso. Non resta che aggiornare il main menu.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): ... def main(): key = None pass_file = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create new password file. 4. Load an existing password file. 5. Add a new service. 6. List all services in the current password file. 7. Get a service (username and password). (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": ... elif choice == "4": ... elif choice == "5": ... elif choice == "6": ... elif choice == "7": if key and pass_dict: list_service(pass_dict, key) name_service = input("Enter the name of the service you want to retrieve: ") get_service(name_service, pass_dict, key) else: print("Please generate or load a valid key and a password file!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Siamo quasi giunti alla fine, questa e l'ultima funzionalità che serve effettivamente al password manager per definirsi tale. Ovvero la funzione delete_service(), che come suggerisce il nome serve a cancellare un servizio esistente dal password file.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): ... def delete_service(name_service, pass_dict, key, path): fernet = Fernet(key) search = False for i in pass_dict: try: service = fernet.decrypt(i.encode()).decode() except InvalidToken: print("Invalid key for this password file!") return if service == name_service: search = True break if search: del pass_dict[i] print(f"Service {name_service} delited!") update_pass_file(path, pass_dict, 'w') else: print("Service not fount!") def main(): ... main()
Gli argomenti per questa funzione sono molto simili a quelli di get_service() infatti abbiamo: name_service per la ricerca del servizio da eliminare, pass_dict, la chiave e il path, per la modifica del password file. La logica di questa funzione è molto simile a get_service(), con la differenza che elimina un servizio, in più per cercare ed eliminare la voce corretta da pass_dict, ho adottato una logica diversa, questo perché in python non e possibile iterare su di un dizionario ed eliminare o aggiungere una voce al contempo. Altrimenti si va incontro al seguente errore:
RuntimeError: dictionary changed size during iteration
Per evitare ciò, ho optato per una logica leggermente diversa. Per prima cosa dichiaro una variabile search che setto a False, subito dopo inizio ad iterare con un ciclo for su ogni elemento di pass_dict, se il ciclo trova una corrispondenza con il servizio da eliminare, setta la variabile search su True e si ferma. Se search esiste (True) elimina il servizio corrispondente, in caso contrario stampa un avviso che il servizio non è stato trovato. Questo tipo di ciclo è molto comune in Python quando si ceracano elementi in dizionari o liste, ed è molto utile conoscerne il funzionamento. Ora non resta che aggiornare il main menu.
from cryptography.fernet import Fernet, InvalidToken def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): ... def main(): key = None pass_file = None pass_dict = {} while True: print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. 4. Load an existing password file. 5. Add a new service. 6. List all services in the current password file. 7. Get a service (username and password). 8. Delete a service. (q) Quit.\n""") choice = input("Enter your choice: ") if choice == "1": ... elif choice == "2": ... elif choice == "3": ... elif choice == "4": ... elif choice == "5": ... elif choice == "6": ... elif choice == "7": ... elif choice == "8": if key and pass_dict: list_service(pass_dict, key) name_service = input("Enter the name of the service you want to delete: ") delete_service(name_service, pass_dict, key, pass_file) else: print("Please generate or load a valid key and a password file!") elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Per rendere il programma più intuitivo e pulito in tutti i suoi output, ho deciso di includere un infojob per dare più informazioni all'utente, va bene essere minimali ma non troppo. In più ora come ora riempie un po' troppo il terminale dei vecchi output, quindi definisco una funzione cls() per pulire il terminale dai vecchi output.
from cryptography.fernet import Fernet, InvalidToken def cls(): print("\x1b[H\x1b[2J") def info_job(name_key, pass_file): if name_key: print(f"\nKey: {name_key}") else: print("\nKey not loaded") if pass_file: print(f"Password file: {pass_file}\n") else: print("Password file not loaded\n") def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): ... def main(): ... main()
info_job() prende come argomenti name_key, che, da ora diventerà una variabile locale alla funzione main(), e pass_file che già lo è, con due semplici blocchi condizionali verifica se il password file o la chiave sono caricati, e produce i corrispettivi output. Par la funzione cls() invece, uso due escape sequence. Tali sequenze sono comandi particolari che eseguono azioni specifiche nei terminali, nello specifico ho utilizzato quelle per i VT100 terminal, che da miei test funzionano sulla maggior parte dei terminali, sia Linux che Windows, (su Windows, nello specifico sia su cmd che su PowerShell). Nel caso specifico '\x1b' indica il carattere da tastiera ESC (escape) in codifica esadecimale, seguito dai carattere [H imposta la posizione del cursore alla colonna corrente, mentre [2J cancella l'intero schermo. Non resta che aggiornare per l'ultima volta il main menu.
from cryptography.fernet import Fernet, InvalidToken def cls(): ... def info_job(name_key, pass_file): ... def create_key(path): ... def load_key(path): ... def update_pass_file(path, pass_dict, mode): ... def create_new_pass_file(path, key, pass_dict, service, username, password): ... def add_service(path, key, pass_dict, service, username, password): ... def list_service(pass_dict, key): ... def get_service(name_service, pass_dict, key): ... def main(): cls() key = None name_key = None pass_file = None pass_dict = {} while True: info_job() print("""Menu: 1. Create a new key. 2. Load an existing key. 3. Create a new password file. 4. Load an existing password file. 5. Add a new service. 6. List all services in the current password file. 7. Get a service (username and password). 8. Delete a service. (q) Quit.\n""") choice = input("Enter your choice: ") cls() if choice == "1": ... elif choice == "2": ... elif choice == "3": ... elif choice == "4": ... elif choice == "5": ... elif choice == "6": ... elif choice == "7": ... elif choice == "8": ... elif choice == "q": print("Bye bye") break else: print("Invalid choice!") main()
Siamo giunti alla fine, in quest'ultima sezione voglio solo dare qualche informazione in merito alle criticità di questo password manager. L'intero programma si basa sulla chiave e su un password file; quindi, se vi venisse l'idea di usarlo effettivamente (cosa che sconsiglio vivamente), tenete ben presente che con la chiave giusta chiunque è in grado di decifrare i dati nel file, specie se tenete chiave e file nello stesso dispositivo, questo è più un esercizio didattico che un vero e proprio password manager.
Se siete arrivati fin qui, ci tengo molto a ringraziarvi per l'attenzione, se volete lasciarmi un feedback mi farebbe molto piacere, sia esso una critica costruttiva o qualsiasi dubbio su qualche parte del progetto. In futuro ho in mente di fare un programma simile ma in c, sicuramente con una logica migliore a più basso livello. Per un saluto da Kid The Hack.
Data pubblicazione:
Autore: Riccardo Loddo Alias: Kid The Hack