Build a Secure CLI Password Manager with Python [Guide]

Cryptographic Encryption Key and Security Vault Python Cryptography

Storing passwords, API tokens, and private SSH keys in unencrypted plaintext files, sticky notes, or shell environment scripts creates an existential security hazard. If malware gains user-level execution on your machine or an attacker exfiltrates your disk image, plaintext credentials result in total compromise.

In this guide, you will build a secure, zero-knowledge command-line password manager using Python's industry-standard cryptography library. We will utilize PBKDF2-HMAC-SHA256 for key derivation with 600,000 hashing rounds and Fernet AES-256 GCM authenticated encryption to guarantee confidentiality and tamper-proof integrity.


Cryptographic Vault Architecture


Prerequisites & Installation

cryptography provides industry-standard primitives including PBKDF2HMAC and Fernet AES-256.

pip install cryptography
ComponentStandard / AlgorithmSecurity SpecificationDefensive Role
Key Derivation FunctionPBKDF2-HMAC-SHA256600,000 iterations + 128-bit random saltSlows down GPU/ASIC brute-force dictionary attacks on master password
Symmetric CipherAES-256 GCM (Fernet)256-bit encryption key + HMAC verificationProvides authenticated encryption (confidentiality and anti-tamper)
Salt Generationos.urandom(16)Cryptographically secure pseudo-random number generator (CSPRNG)Prevents rainbow table lookup attacks
Vault PersistenceJSON over Encrypted Binary PayloadZero plaintext stored on diskGuarantees zero-knowledge storage

How Authenticated Key Derivation Works

A master password cannot be directly used as an AES encryption key because human passwords lack sufficient entropy. A Key Derivation Function (KDF) stretches the user's password into a fixed 256-bit cryptographic key.

By enforcing 600,000 rounds of SHA-256 hashing alongside a unique random salt, an attacker attempting to brute-force the master password must compute hundreds of thousands of hashes for every single password guess.


Building the Encrypted Vault Application in Python

import base64
import os
import json
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes

class SecurePasswordVault:
    """Zero-knowledge encrypted password vault using AES-256 GCM."""
    def __init__(self, vault_filepath="vault.enc"):
        self.vault_filepath = vault_filepath

    def _derive_key(self, master_password: str, salt: bytes) -> bytes:
        """Derives a 256-bit key from master passphrase using 600,000 PBKDF2 iterations."""
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=600_000,
        )
        return base64.urlsafe_b64encode(kdf.derive(master_password.encode('utf-8')))

    def save_vault(self, data: dict, master_password: str):
        """Encrypt and write vault data to disk."""
        salt = os.urandom(16)
        key = self._derive_key(master_password, salt)
        fernet = Fernet(key)
        
        raw_json = json.dumps(data).encode('utf-8')
        encrypted_payload = fernet.encrypt(raw_json)
        
        # Store salt (16 bytes) followed by ciphertext
        with open(self.vault_filepath, "wb") as f:
            f.write(salt + encrypted_payload)
        print(f"[Vault Saved] Encrypted {len(data)} credential records.")

    def load_vault(self, master_password: str) -> dict:
        """Read and decrypt vault data from disk."""
        if not os.path.exists(self.vault_filepath):
            return {}
            
        with open(self.vault_filepath, "rb") as f:
            content = f.read()
            
        salt = content[:16]
        encrypted_payload = content[16:]
        
        key = self._derive_key(master_password, salt)
        fernet = Fernet(key)
        
        decrypted_json = fernet.decrypt(encrypted_payload).decode('utf-8')
        return json.loads(decrypted_json)

if __name__ == "__main__":
    vault = SecurePasswordVault("my_secrets.enc")
    pwd = "MasterPassword!2026Secure"
    
    # Store credentials
    secrets = {
        "github.com": {"user": "alex_dev", "token": "ghp_9847128947192471"},
        "aws_console": {"user": "admin", "token": "AKIAIOSFODNN7EXAMPLE"}
    }
    vault.save_vault(secrets, pwd)
    
    # Decrypt and retrieve
    unlocked = vault.load_vault(pwd)
    print("Decrypted Vault Contents:", unlocked)

Operational Security and Memory Safety

  • Prevent Memory Swapping: Sensitive passphrases stored in Python string objects remain in memory until garbage collected. For maximum security, avoid printing plaintext credentials to stdout.
  • Never Hardcode Salts: Each time you save or re-encrypt the vault, generate a fresh random salt via os.urandom(16) to ensure identical credentials produce completely different ciphertexts.
  • Regular Vault Backups: Because AES-256 GCM is authenticated, any byte corruption on disk will cause decryption to fail entirely. Keep timestamped encrypted backups.

Frequently Asked Questions

Q: What happens if I forget the master password?
A: Because this is a zero-knowledge architecture with no backdoor or recovery key, the data cannot be decrypted by anyone if the master password is lost.

Q: Is Fernet quantum-resistant?
A: Fernet utilizes AES-256 symmetric encryption, which offers 128-bit quantum security against Grover's algorithm and remains secure against foreseeable quantum threats.

Post a Comment

0 Comments