Most password managers today ship as JavaScript or memory-safe language applications. Building one in C++ is a different exercise entirely. Manual memory management forces you to know exactly where sensitive data lives, how long it stays there, and what happens when you try to erase it. Even a simple C++ password manager becomes a study in defensive design: every byte of plaintext is a liability, and the operating system will not help you keep secrets.
Core Architecture: Separation of Duties
A secure password management system is not a monolithic application. Split it into three logical components: a vault storage layer, a cryptographic engine, and a user interface. The vault storage layer handles serialization and deserialization of encrypted data to disk. The cryptographic engine performs key derivation, encryption, and decryption. The user interface accepts input and displays output. The critical rule is that plaintext passwords never cross the boundary into the storage layer; they are encrypted inside the cryptographic engine and only ciphertext is passed outward.
In C++, class design can enforce this separation. The vault class should only accept std::vector<unsigned char> or similar byte containers holding ciphertext. The cryptographic engine class should be the only place where a plaintext std::string holding a password briefly exists. This makes it harder to accidentally log a password or write it to a swap file.
Key Derivation: Argon2 and the Memory-Hard Advantage
A common mistake in educational implementations is hashing the master password with a single round of SHA-256. That is insufficient. A production-grade system uses a memory-hard key derivation function. Argon2id is the current standard, designed to resist both side-channel and GPU-based attacks. The memory-hard property means an attacker cannot easily parallelize brute-force attempts on specialized hardware.
When integrating Argon2 into a C++ project, the reference implementation in C provides a clean API. The function expects a password, a salt, and parameters for time cost, memory cost, and parallelism. The salt must be generated from a cryptographically secure random number generator. On Linux, this means reading from /dev/urandom or using the getrandom() syscall. The output is a derived key, typically 32 bytes for AES-256. The salt is stored alongside the ciphertext in the vault file; it is not secret, but it must be unique per vault.

Encryption: Authenticated Encryption with Associated Data
Encryption alone is not enough. Without authentication, an attacker can modify ciphertext and the application will decrypt garbage or, worse, reveal information through error oracles. The correct primitive is Authenticated Encryption with Associated Data (AEAD). AES-256-GCM is a widely available choice, supported by OpenSSL's EVP API. The associated data can include the vault metadata, such as the version number and the salt, binding the ciphertext to its context.
In C++, the OpenSSL EVP functions require careful context management. A typical encryption operation looks like this: create a cipher context, initialize it with the key and a 12-byte initialization vector (IV), provide the associated data, feed the plaintext, and finalize to retrieve the authentication tag. The IV must be unique per encryption operation. Using a counter or generating a random IV and storing it with the ciphertext are both valid approaches. The tag is appended to the ciphertext and verified during decryption before any plaintext is returned.
Memory Sanitization: Locking and Zeroing
This is where C++ demands extraordinary discipline. When a password is stored in a std::string, the memory is managed by the allocator. When the string goes out of scope, the memory is freed but the contents are not erased. The password remains in the process's memory, potentially swapped to disk by the operating system's virtual memory manager. Two techniques mitigate this.
First, sensitive buffers should be allocated using mlock() on POSIX systems or VirtualLock() on Windows. This prevents the memory from being paged to disk. Second, before freeing the buffer, the contents must be overwritten with zeros or random bytes. The sodium_memzero() function from libsodium is designed for this purpose and is resistant to compiler optimizations that might remove a seemingly redundant memset() call. In pure C++, a volatile pointer to the buffer can force the write, but relying on a well-tested library is safer.
The same approach applies to the master password entered by the user. Read it into a locked buffer, use it for key derivation, and immediately zero it. The derived key should be held in memory only as long as the vault is unlocked.

Vault File Format and Integrity
The vault file on disk is a structured binary file. A simple but workable format begins with a fixed header: a magic number to identify the file type, a version byte, the salt used for key derivation, and the length of the encrypted payload. The payload follows, containing the encrypted and authenticated data. The authentication tag is stored at the end of the payload section.
When the application reads the vault, it parses the header, extracts the salt, derives the key from the user's master password, and then attempts to decrypt and verify the payload. If the tag verification fails, the application reports an incorrect password or corrupted file without leaking further details. The error message must be identical in both cases to prevent an attacker from distinguishing between them.
For the internal plaintext structure, a simple key-value store is sufficient. Each entry consists of a service name, a username, and a password. The entire collection is serialized into a contiguous buffer, encrypted, and written to disk. When the user adds a new entry, the entire vault is re-encrypted with a new IV. This prevents an attacker from learning how many entries exist by observing file size changes over time, though the total size still reveals an upper bound.
Clipboard and User Interface Considerations
Even a perfectly encrypted vault is useless if plaintext leaks through the user interface. A common convenience feature is copying a password to the clipboard. On Linux, X11 and Wayland clipboards are globally accessible to any running application. A password manager should clear the clipboard after a short timeout, typically 30 seconds, and should offer an option to disable clipboard copying entirely. The QClipboard class in Qt or a direct X11 API call can be used, but the clearing mechanism must be reliable.
Displaying passwords on screen is similarly risky. The default should be to hide them behind asterisks or not show them at all, with an explicit user action required to reveal a password. This is not paranoia; it is a defense against shoulder surfing and screen capture utilities that may be running in a compromised desktop session.
Building and Linking Against Cryptographic Libraries
Linking against OpenSSL or libsodium in a C++ project is straightforward with CMake. The find_package command locates the libraries, and target_link_libraries adds them to the build. Link dynamically against the system's OpenSSL to receive security updates automatically, rather than statically linking a potentially outdated version. For a deeper dive into structuring a C++ project with external dependencies, the post on Choosing the Right C++ IDE for Your Development Needs covers workspace configuration and compiler flags that help maintain a clean build environment.
Testing in a Safe Environment
Testing a password manager requires a controlled environment. A dedicated virtual machine with no network access is ideal. Inspect the vault file with a hex editor to verify that no plaintext appears. Valgrind or AddressSanitizer can detect memory leaks, but more importantly, custom instrumentation can verify that sensitive buffers are zeroed after use. The test suite should include edge cases: empty vaults, very large password entries, and rapid locking and unlocking cycles. The goal is to ensure that the application fails closed—any unexpected error should result in a locked vault, not a plaintext dump.
Writing a password manager in C++ is an intensive lesson in applied cryptography and systems programming. The final product will not compete with existing open-source tools, but the process teaches habits essential for any developer working on security-critical software: distrust of the memory allocator, respect for authentication tags, and a relentless focus on the lifetime of every byte in memory.
