Building a secrets manager
I was working on a side project and needed somewhere to store env vars. You know the typical day DATABASE_URL, API_KEY, all that stuff you definitely shouldn't commit to git (I swear if you do you're done).
Looked at Infisical. Cool product. But holy shit I don't think i need that for my side project. I just want to store some strings.
As developers we love to reinvent the wheel don't we? So yeah of course I'm going to think of a implementation.
The idea
Just by running this you can have your env injected:
inject cool-app --env=prod bun start // automatic injection!So basically it's gonna fetch secrets from an API then inject them as env vars. Isn't that cool? Simple as that.
It's dead simple if you think about it:
CLI -> API Server -> DatabaseProjects can have multiple environments and envs contain secrets. Secrets are key-value pairs and tokens control access. That's the whole data model.
We need E2E encryption
Here's the thing about most secret managers: they encrypt on the server. Your secrets arrive in plaintext over HTTPS (I know it TLS isn't plain text you), the server encrypts it, stores it. When you need it, the server decrypts it, sends it back.
This means the server can always read your secrets. If someone hacks it, compromises the database, they can read everything. Amazing isn't it?
I wanted something where the server literally cannot read your secrets.
How it works
Encryption happens entirely on your machine and same with decryption:
YOUR MACHINE
password: "VerySafePassword"
|
v
Argon2id (memory-hard KDF) + random salt
|
v
256-bit AES key (derived, never stored)
|
v
AES-256-GCM encryption + random nonce
|
v
ciphertext = salt (16) + nonce (12) + encrypted data + tag
|
v
SERVER
receives: "yiF0cj2eF1We/HQrHgpYpP..." (base64 blob)
stores: exactly that blob
can decrypt: not at allThe server is now just a dumb storage layer. It stores blobs. It returns blobs. That's it.
Why these algorithms?
Argon2id: This is the current OWASP recommendation for password-based key derivation. It's memory-hard, meaning it requires a lot of RAM to compute. This makes it resistant to GPU/ASIC attacks since those have limited memory per core. Each guess takes ~100ms and needs 64MB of RAM.
AES-256-GCM: This ones the gold standard for symmetric encryption. GCM mode provides both encryption and authentication. If someone tampers with the ciphertext, decryption fails. No need for a separate HMAC.
Random salt per secret: The salt is generated fresh for each encryption. Same password + same plaintext = different ciphertext every time. This prevents attackers from identifying duplicate secrets.
Random nonce per encryption: GCM requires a unique nonce for each encryption with the same key. We generate 12 random bytes. Reusing a nonce with the same key would be catastrophic (breaks all security guarantees), so randomness is critical.
What the database looks like
projects table;
id name created_at
6d4e3... myapp 2025-12-02T08:11Z
secrets table;
id environment_id key encrypted_value
bf709... 3b51d0... NODE_ENV yiF0cj2eF1We/HQrHgpYpP9bkis8aJ3C7B...That encrypted_value is completely unknown to the server. It’s just this
[salt 16B][nonce 12B][ciphertext][tag 16B] → base64Without the password it's literally just random noise. Good luck decrypting that.
Show me the code
There is none I was lazy to write the actual implementation but this will serve as the whitepaper if I ever do!