Developing a Command Line Password Manager in Python with Encryption
Secure password management is a critical component of digital security. Reusing passwords or using weak ones across multiple services significantly increases the risk of account compromise. While graphical password managers offer convenience, a command-line alternative can be valuable for developers, system administrators, or those who prefer terminal-based workflows. Creating such a tool involves understanding data storage, user input handling, and, most importantly, robust encryption techniques.
A command-line password manager stores credentials (like usernames and passwords) securely within a file on the local system, accessible and manageable via terminal commands. Encryption is essential to protect this stored data from unauthorized access, even if the file is obtained by a third party. The process involves using a master password provided by the user to encrypt and decrypt the stored credentials.
Essential Concepts for Secure Password Management
Building a secure password manager, even a simple command-line one, requires understanding several core cryptographic and programming concepts:
- Master Password: A single, strong password the user remembers and uses to unlock the entire password database. The security of the stored credentials hinges entirely on the strength and secrecy of this master password.
- Key Derivation Function (KDF): Passwords, especially user-chosen ones, are typically not suitable for direct use as cryptographic keys. KDFs are functions designed to derive a strong, fixed-length cryptographic key from a password or passphrase, often incorporating a
saltand computationally intensive work factors (iterations, memory usage) to resist brute-force attacks. Examples include PBKDF2, Scrypt, and Argon2. Using a KDF is crucial; deriving a key directly from a password via simple hashing is insecure. - Salt: A unique, random piece of data associated with each password derivation. The salt is stored alongside the encrypted data (it does not need to be secret). Its purpose is to ensure that identical passwords produce different derived keys and encrypted outputs, preventing attackers from using precomputed tables (rainbow tables) and requiring them to crack each password individually.
- Symmetric Encryption: An encryption method where the same key is used for both encryption and decryption. This is suitable for encrypting the bulk of the password data. Algorithms like AES (Advanced Encryption Standard) are widely used and considered secure when implemented correctly. The security depends entirely on keeping the encryption key secret.
- Authenticated Encryption: A method that provides both confidentiality (encryption) and integrity/authenticity (ensuring the data hasn’t been tampered with). Using an authenticated encryption mode or combining a symmetric cipher with a message authentication code (MAC) is best practice. Libraries often provide this as a single function.
- Secure Input: When prompting the user for the master password, the input should not be echoed to the terminal to prevent shoulder-surfing.
Choosing Python Libraries for Encryption
Python offers several libraries for cryptographic operations. For building a password manager, the cryptography library is a recommended choice. It provides both high-level recipes (like Fernet) and low-level interfaces to common cryptographic primitives.
- The
cryptographylibrary is developed and maintained by cryptographers and is designed with security best practices in mind. It is preferred over older or built-in modules likehashlib(which only provides hashing, not encryption) or deprecated ones. - Specifically, the
cryptography.hazmat.primitives.kdfmodule contains implementations of KDFs like Scrypt and PBKDF2. - The
cryptography.fernetmodule provides Fernet, an opinionated authenticated encryption scheme that is suitable for this purpose. It uses AES in CBC mode with HMAC for authentication and includes the IV (Initialization Vector) and timestamp within the ciphertext. Fernet keys are derived from a base URL-safe encoding, but the underlying secret key for Fernet needs to be a 32-byte value, which can be derived from the master password using a KDF.
Implementing the Command Line Password Manager
Building the manager involves several steps, focusing on the core encryption/decryption and data handling logic. This outline assumes storing password entries as a Python dictionary that gets serialized (e.g., to JSON) before encryption.
Step 1: Project Setup and Dependencies
Ensure Python is installed. Install the cryptography library using pip:
pip install cryptographyStep 2: Designing the Data Structure
A simple structure to store multiple password entries is a dictionary where keys are service names (e.g., “google”, “github”) and values are dictionaries containing the username and password.
{ "service_name_1": { "username": "user1", "password": "password123" }, "service_name_2": { "username": "user2", "password": "anotherpassword" } # ... more entries}This dictionary will be converted to a string (e.g., JSON), encrypted, and saved to a file.
Step 3: Handling the Master Password and Key Derivation
The master password is the user’s secret. It should not be stored. Instead, a cryptographic key is derived from it using a KDF and a salt. The salt must be stored alongside the encrypted data (or within the encrypted file header) so the correct key can be derived later for decryption.
- Generate a Salt: A unique salt (at least 16 bytes is recommended) should be generated when the password manager is initialized or when the master password is first set.
import ossalt = os.urandom(16)# Store this salt alongside the encrypted data
- Derive the Key: Use a KDF like Scrypt or PBKDF2. Scrypt is often preferred due to its memory hardness. The KDF requires the master password (as bytes), the salt, and work factors (n, r, p for Scrypt; iterations for PBKDF2). These work factors should be chosen based on desired security levels and performance constraints. Higher values mean more security but slower derivation. The output is the derived key of the required length (e.g., 32 bytes for Fernet).
from cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.kdf.scrypt import Scryptfrom cryptography.hazmat.backends import default_backendimport getpass # For secure password input# --- When setting up or loading ---# Assume master_password_str is obtained securely via getpassmaster_password_bytes = master_password_str.encode('utf-8')# Assume salt is loaded from file or generated if newkdf = Scrypt(salt=salt,length=32, # For Fernetn=2**14, # CPU/memory cost - adjust based on testingr=8, # Block sizep=1, # Parallelization factorbackend=default_backend())try:derived_key = kdf.derive(master_password_bytes)# Use derived_key for encryption/decryptionexcept InvalidKeyException:# Master password was incorrect during derive/verifyprint("Error: Incorrect master password.")# Exit or handle failure
- Verify the Key (Optional but Recommended during Decryption): KDF objects often have a
verifymethod which performs the derivation and checks if the result matches a known derived key (though in a password manager, verifying against the data’s integrity after decryption is often sufficient, provided the KDF parameters and salt are stored correctly). Thederivemethod is used when you need the key to encrypt or decrypt.
Step 4: Encrypting and Decrypting Data
Using the derived key with Fernet:
- Encryption: Serialize the data structure (e.g., to a JSON string), encode it to bytes, and then use the Fernet object created with the derived key to encrypt it.
import jsonfrom cryptography.fernet import Fernet# Assume data_dict is the Python dictionary of credentialsdata_bytes = json.dumps(data_dict).encode('utf-8')fernet = Fernet(derived_key)encrypted_data = fernet.encrypt(data_bytes)# Save salt and encrypted_data to a file
- Decryption: Load the salt and encrypted data from the file. Obtain the master password from the user and derive the key using the same salt and KDF parameters. Use the Fernet object with this key to decrypt the data. Handle potential
InvalidTokenexceptions, which indicate either the wrong key was used (wrong master password) or the data was corrupted.# Assume salt and encrypted_data are loaded from file# Assume derived_key is obtained by deriving from user input master password and loaded saltfernet = Fernet(derived_key)try:decrypted_bytes = fernet.decrypt(encrypted_data)data_dict = json.loads(decrypted_bytes.decode('utf-8'))# Data successfully decrypted and loadedexcept InvalidToken:print("Error: Could not decrypt data. Incorrect master password or corrupted data.")# Exit or handle failure
Step 5: File Handling
The salt and encrypted data need to be stored persistently. A common approach is to store them together in a single file. A simple format could be storing the salt, followed by the encrypted data, perhaps separated by a delimiter or with the salt length indicated. Storing them as part of a structured format like JSON (with the salt encoded, e.g., in base64) is also feasible.
# Example structure for storage (can be JSON, or a custom binary format)storage_data = { "salt": salt.hex(), # Store salt as hex or base64 "encrypted_data": encrypted_data.hex() # Store encrypted data as hex or base64}# Write storage_data (as JSON string) to file# When loading, read JSON, decode salt and encrypted_data back to bytesUsing hexadecimal or base64 encoding for binary data (like salt and ciphertext) when storing in a text format like JSON or a plain text file is standard practice.
Step 6: Building the Command-Line Interface
Implement functions to handle user commands:
add: Prompt for service name, username, and password. Load data (decrypt), add the new entry, save data (encrypt).get: Prompt for service name. Load data (decrypt), retrieve and display the username and password for the service.list: Load data (decrypt), display all stored service names.set-master-password: For initial setup or changing the master password. This requires re-encrypting the entire dataset with the new password and a new salt.
Use getpass.getpass() to securely prompt for the master password and sensitive credentials.
Example Snippet: Basic Get Function
This illustrates the core load, decrypt, and retrieve logic.
import jsonimport getpassimport osfrom cryptography.fernet import Fernet, InvalidTokenfrom cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.kdf.scrypt import Scryptfrom cryptography.hazmat.backends import default_backend
DATA_FILE = 'passwords.dat' # File to store encrypted data and saltKDF_N = 2**14 # Scrypt parametersKDF_R = 8KDF_P = 1KEY_LENGTH = 32 # For Fernet
def load_data(master_password): """Loads and decrypts data from the storage file.""" if not os.path.exists(DATA_FILE): return None # File doesn't exist
try: with open(DATA_FILE, 'r') as f: storage_content = json.load(f)
salt = bytes.fromhex(storage_content['salt']) encrypted_data = bytes.fromhex(storage_content['encrypted_data'])
kdf = Scrypt(salt=salt, length=KEY_LENGTH, n=KDF_N, r=KDF_R, p=KDF_P, backend=default_backend()) key = kdf.derive(master_password.encode('utf-8'))
fernet = Fernet(key) decrypted_data_bytes = fernet.decrypt(encrypted_data) data_dict = json.loads(decrypted_data_bytes.decode('utf-8')) return data_dict
except (FileNotFoundError, json.JSONDecodeError, InvalidToken, Exception) as e: # Catch file errors, JSON errors, incorrect password (InvalidToken), etc. print(f"Error loading data: {e}") return None
def get_password(): """Prompts for service and displays credentials.""" master_pwd = getpass.getpass("Enter master password: ") data = load_data(master_pwd)
if data is None: print("Could not load or decrypt data.") return
service = input("Enter service name: ").lower()
if service in data: print(f"Service: {service}") print(f"Username: {data[service]['username']}") print(f"Password: {data[service]['password']}") else: print(f"Service '{service}' not found.")
# --- Basic execution example ---# In a real application, this would be driven by command-line arguments# if __name__ == "__main__":# # Example usage (requires DATA_FILE with salt and encrypted_data)# # Need functions for add, list, and initial setup first# get_password()Security Considerations and Best Practices
Developing a secure tool requires more than just using encryption functions.
- Protect the Master Password: The master password is the single point of failure. Choosing a strong, unique master password is paramount.
- Do Not Store the Master Password: The application must never store the master password itself, neither in memory long-term nor on disk. Prompt the user for it each time the application needs to access the decrypted data.
- Securely Handle Derived Keys: The derived encryption key should be kept in memory only as long as needed and cleared afterwards if possible (though Python’s garbage collection makes deterministic memory clearing difficult).
- Store Salt and KDF Parameters: The salt used for key derivation and the parameters for the KDF (N, R, P for Scrypt; iterations for PBKDF2) must be stored alongside the encrypted data. These are not secret, but are necessary to derive the correct key. Storing them with the encrypted data allows decryption without hardcoding these values.
- Use a Trusted Cryptography Library: Implementing cryptographic primitives or protocols from scratch is highly discouraged. Use well-vetted libraries like Python’s
cryptography. - File Permissions: Ensure the file storing the encrypted data has appropriate file system permissions to restrict access to the current user.
- Backup: Consider secure backup strategies for the encrypted data file. Losing the file means losing all passwords. Losing the file and forgetting the master password makes the data unrecoverable.
Real-World Application
A command-line password manager is particularly useful in specific scenarios:
- Developer Workflows: Storing database credentials, API keys, or SSH passphrases locally for quick access within scripts or command-line tools. This avoids hardcoding sensitive information directly in scripts.
- Server Management: Accessing credentials on servers or minimal environments where a full GUI browser extension or application is not available or desirable.
- Automation: Integrating password retrieval into secure automation scripts (though careful consideration of how the master password is provided to the script is needed – avoiding plain text in scripts is crucial).
- Offline Use: Providing access to passwords without relying on cloud synchronization, which might be a preference for some users concerned about cloud security.
Such a tool, while perhaps less feature-rich than commercial options (which offer sync, sharing, browser integration), provides a transparent, local-first approach with control over the underlying implementation details.
Key Takeaways
- Creating a secure command-line password manager requires robust encryption.
- A master password is used to derive a strong encryption key using a Key Derivation Function (KDF) like Scrypt or PBKDF2.
- A unique salt must be used with the KDF and stored alongside the encrypted data.
- Symmetric encryption (e.g., AES via Fernet in
cryptography) is used to protect the password data with the derived key. - The
cryptographyPython library is recommended for implementing secure encryption and key derivation. - Sensitive input like the master password should be handled securely using tools like
getpass. - The master password should never be stored; it must be provided by the user for each access.
- Security relies on a strong master password, correct use of cryptographic primitives, and protecting the encrypted data file.