A simple C++ library for securely storing secrets using the operating system's or desktop manager's vault.
I started working on a new command-line tool in C++20 that connects to various servers using gRPC and HTTPS. All these servers require authentication, using different methods:
- HTTP Basic Authentication
- API keys
- X.509 certificate/key pairs
Since this tool is meant for production environments, I don't want credentials to be stored in plain text on disk. I needed a simple, cross-platform way to handle secrets securely.
On Linux, the library uses libsecret, which talks to the desktop Secret Service implementation.
Beta
| Platform | Status |
|---|---|
| Linux (KDE) | ✅ Works through Secret Service |
| Linux (GNOME) | ✅ Works with GNOME Keyring (GNOME Desktop) |
| macOS | ✅ Works (Keychain Services) |
| Windows 10 | ✅ Works (Credential Manager) |
| Windows 11 | ✅ Works (Credential Manager) |
Each namespace lives under the user’s application data directory as:
- Linux:
~/.local/share/safekeeping/<namespace>/vault.dbunlessXDG_DATA_HOMEis set - macOS:
~/Library/Application Support/safekeeping/<namespace>/vault.db - Windows:
%APPDATA%/safekeeping/<namespace>/vault.db
Originally, the library stored secrets in the system's vault. However, Windows Credential Manager has a 512-byte limit on secrets, so I was unable to store some PKI certificates, which I normally use for authentication.
In the current version of this library, the actual secrets are stored as encrypted blobs in a local SQLite database. Only the decryption key is stored (per namespace) in the system’s vault.
On Linux, the current design stores one vault item per namespace unlock slot rather than one vault item per secret. That item is stored through libsecret.
By default, the Linux vault root name is com.jgaa.SafeKeeping. Applications can override it during startup with SafeKeeping::setLinuxVaultRootName(...) to avoid collisions or to group entries under an application-specific service name.
This new design also makes it possible to generate a recovery key, as well as use an additional password/passphrase, so data can be extracted from the vault (for example, from a backup) even if the system's secret vault is lost. These are optional features, but they may prove quite useful.
The SQLite database stores:
- encrypted secret names
- encrypted secret values
- encrypted descriptions
- wrapped copies of the namespace encryption key for each unlock method
Build-time dependencies:
- C++20 compiler
- CMake
- SQLite3
- libsodium
- GoogleTest (if building tests)
Platform dependencies:
- Linux:
libsecret - macOS: Security / CoreFoundation frameworks
- Windows: Credential Manager /
Advapi32
Minimum practical install:
sudo pacman -S --needed base-devel cmake ninja git sqlite libsodium libsecret gtestFor a Linux vault provider, install one of:
sudo pacman -S --needed gnome-keyringor
sudo pacman -S --needed kwalletor
sudo pacman -S --needed keepassxcBuild dependencies:
sudo apt update
sudo apt install -y build-essential cmake ninja-build pkg-config git \
libsqlite3-dev libsodium-dev libsecret-1-dev libgtest-devFor a Linux vault provider at runtime, install one of:
sudo apt install -y gnome-keyringor
sudo apt install -y kwalletmanagerCurrent Ubuntu LTS releases use the same core development package names as Debian:
sudo apt update
sudo apt install -y build-essential cmake ninja-build pkg-config git \
libsqlite3-dev libsodium-dev libsecret-1-dev libgtest-devFor a Linux vault provider at runtime, install one of:
sudo apt install -y gnome-keyringor
sudo apt install -y kwalletmanagerBuild dependencies:
sudo dnf install -y gcc-c++ cmake ninja-build pkgconf-pkg-config git \
sqlite-devel libsodium-devel libsecret-devel gtest-develFor a Linux vault provider at runtime, install one of:
sudo dnf install -y gnome-keyringor
sudo dnf install -y kwalletcmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failureThe rebooted API is centered around namespace lifecycle and explicit unlock methods.
Core operations:
SafeKeeping::createNew(...)SafeKeeping::open(...)SafeKeeping::exists(...)SafeKeeping::removeNamespace(...)storeSecret(...)storeSecretWithDescription(...)retrieveSecret(...)removeSecret(...)listSecrets()
Unlock and slot management:
unlockWithSystemVault()unlockWithPassphrase(...)unlockWithRecoveryKey(...)lock()addSystemVaultSlot()addPassphrase(...)changePassphrase(...)removePassphrase()rotateRecoveryKey()removeRecoveryKey()
See include/safekeeping/SafeKeeping.h for the full interface.
// unchanged- Secret operations require an unlocked namespace.
- Secret values are limited to 10,240 bytes.
- For binary payloads, use
storeSecret(..., std::span<const std::byte>)andretrieveSecretBytes(...). - Instance methods clear
latestError()before each operation and set it on failure. - Secret names are validated and used through an encrypted-record model with a keyed lookup hash.
- Recovery keys are generated once and returned once. They are not retrievable later.
- At least one active unlock slot must remain.
- If the vault material is lost, the passphrase or recovery key can still unlock the namespace.
- If all unlock methods are lost, the data is unrecoverable by design.