Hay un archivo que muchos developers tienen y nadie admite. Se llama keys.txt, o env_backup.txt, o simplemente temp — y adentro hay API keys, tokens, contraseñas de bases de datos. Sin cifrar. Sin contraseña. Ahí nomás, en el escritorio.
Yo lo tuve durante dos años.
No por ignorancia. Sabía que era mala práctica. Pero los gestores de contraseñas comerciales no están pensados para developers: mezclan credenciales web con tokens de producción, no tienen campos para variables de entorno, y tarde o temprano terminan mandando tus datos a una nube que no controlás.
Así que un día borré el archivo y empecé a construir algo propio.
Qué es Ciphie
Ciphie es un gestor de secretos que corre completamente en tu máquina. Sin servidor, sin cuenta, sin suscripción. Lo abrís, guardás tus API keys, tokens y contraseñas, y eso es todo. Por defecto, nada sale de tu computadora — las únicas conexiones de red opcionales son el email de verificación de cuenta y el 2FA por SMS, ambas desactivadas si no las configurás en el .env.
Tiene interfaz de escritorio con tema oscuro, soporte para varios tipos de secretos con campos específicos por categoría, y segundo factor de autenticación con cuatro métodos distintos para elegir al momento del login.
Todavía no está publicado — estoy terminando algunos detalles antes de soltar la primera versión. Pero quería contar la historia antes de eso.
El problema con las alternativas
Antes de construir algo, siempre conviene ver qué existe. En mi caso, las opciones eran:
Gestores comerciales (1Password, Bitwarden, Dashlane): buenos productos, pero orientados a contraseñas web. Para un developer que maneja decenas de API keys con campos como endpoint, región, scope o environment, la experiencia es torpe. Y dependen de la nube.
Soluciones de infraestructura (HashiCorp Vault, AWS Secrets Manager): excelentes para equipos y producción, completamente sobredimensionados para un developer individual que quiere guardar sus keys de forma segura durante el desarrollo.
Variables de entorno del sistema: válido, pero sin interfaz, sin organización y sin cifrado en reposo.
Lo que quería era simple: una app de escritorio, local, cifrada, pensada para developers. No encontré exactamente eso, así que lo construí.
Las decisiones que importan
Construir algo de seguridad obliga a tomar decisiones que en otros proyectos son opcionales. Acá están las que más me marcaron.
Una dependencia obligatoria, el resto opcional
La primera decisión fue de filosofía: mantener el stack al mínimo absoluto. Ciphie tiene una sola dependencia obligatoria — cryptography, el paquete de la Python Cryptographic Authority. Todo lo demás que podría necesitar (QR codes para TOTP, SMS vía Twilio, Touch ID en macOS) son extras opcionales que se instalan solo si los querés usar:
pip install ciphie # solo lo esencialpip install ciphie[qr] # agrega QR codes para el 2FA con apppip install ciphie[sms] # agrega 2FA por SMS vía Twiliopip install ciphie[all] # todo juntoLa interfaz, la base de datos y el hashing de contraseñas usan módulos que vienen con Python: tkinter, sqlite3, hashlib. Cada dependencia adicional es una superficie de ataque. En un proyecto de seguridad, eso pesa más que la comodidad de una librería extra.
Cifrado real, no teatro de seguridad
Cada secreto se cifra con AES-256-GCM antes de guardarse. No es solo una decisión de algoritmo — es un modo que importa.
GCM (Galois/Counter Mode) hace dos cosas: cifra el dato y lo autentica. Si alguien modifica el archivo de la base de datos directamente, el descifrado no devuelve basura silenciosamente: falla con un error explícito. Eso es la diferencia entre un sistema que detecta manipulación y uno que no.
def cifrar(valor: str) -> str: clave = _get_clave_aes() nonce = os.urandom(12) # 96 bits — tamaño recomendado para GCM aesgcm = AESGCM(clave) cifrado = aesgcm.encrypt(nonce, valor.encode("utf-8"), None) return base64.urlsafe_b64encode(nonce + cifrado).decode("utf-8")Lo otro que importa es el nonce: 12 bytes generados aleatoriamente para cada cifrado individual. Sin eso, dos secretos con el mismo contenido producen el mismo resultado cifrado — y eso rompe la seguridad del esquema completo. Con el nonce aleatorio, cada cifrado es único aunque el contenido sea idéntico.
Las contraseñas no se guardan
Las contraseñas de los usuarios se hashean con PBKDF2-HMAC-SHA256 con 310.000 iteraciones — la recomendación de OWASP para 2023. Cada iteración extra multiplica el tiempo que le lleva a un atacante probar contraseñas en un ataque offline. Con esta configuración, una GPU moderna tardaría meses en fuerza bruta, no segundos.
_ITERATIONS = 310_000
def _hash_password(password: str) -> str: salt = os.urandom(16) clave = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, _ITERATIONS, dklen=64 ) return salt.hex() + ":" + clave.hex()Un detalle que casi ignoro: la comparación del hash usa hmac.compare_digest en lugar del operador == de Python. La diferencia es que == puede terminar antes si los primeros bytes no coinciden, lo que permite medir tiempos y deducir información sobre el hash real. compare_digest siempre tarda exactamente lo mismo, sin importar el resultado.
# ❌ vulnerable a timing attacksif hash_calculado == hash_guardado:
# ✅ tiempo constante, sin importar cuántos bytes coincidanif hmac.compare_digest(hash_calculado, hash_guardado):SQLite es suficiente
No necesité un servidor de base de datos. SQLite corre en proceso, el archivo vive localmente con permisos restringidos — solo el usuario dueño puede leerlo —, y es más que suficiente para este caso de uso. A veces la solución correcta es la más simple.
Lo que sorprende cuando construís algo de seguridad
Hay una diferencia enorme entre saber que algo existe y tener que implementarlo correctamente.
Los timing attacks son un buen ejemplo. Sabía que existían. Pero fue distinto tener que pensar: “si uso == acá, ¿estoy filtrando información?” y tener que buscar la alternativa correcta. La seguridad está llena de esos momentos: detalles pequeños con consecuencias grandes.
Lo mismo con los nonces. O con la diferencia entre cifrado autenticado y no autenticado. O con la cantidad de iteraciones del hash. Ninguno de esos detalles aparece en un tutorial introductorio, pero todos importan en producción.
Construir Ciphie fue, entre otras cosas, un ejercicio de ir más allá de la superficie de cada concepto.
Lo que viene
Antes de publicar la primera versión, estoy terminando algunos ajustes. Cuando esté lista, va a estar disponible en GitHub bajo licencia MIT. Una sola línea para instalar, otra para abrirlo.
Si manejás secretos como developer y te interesa probarlo cuando esté disponible, seguime acá — voy a publicar el lanzamiento en cuanto esté listo.
Y si tenés un keys.txt en el escritorio, borralo ya. No esperés a que salga Ciphie.
Ciphie está construido con Python 3.11+, SQLite, Tkinter y la librería cryptography. El código va a estar disponible en GitHub bajo licencia MIT.
There’s a file many developers have and nobody admits to. It’s called keys.txt, or env_backup.txt, or just temp — and inside there are API keys, tokens, database passwords. Unencrypted. No password. Just sitting there on the desktop.
I had one for two years.
Not out of ignorance. I knew it was bad practice. But commercial password managers aren’t built for developers: they mix web credentials with production tokens, have no fields for environment variables, and sooner or later end up sending your data to a cloud you don’t control.
So one day I deleted the file and started building something of my own.
What is Ciphie
Ciphie is a secrets manager that runs entirely on your machine. No server, no account, no subscription. You open it, save your API keys, tokens and passwords, and that’s it. By default, nothing leaves your computer — the only optional network connections are account verification email and 2FA via SMS, both disabled unless you configure them in the .env.
It has a desktop UI with dark theme, support for multiple secret types with category-specific fields, and two-factor authentication with four different methods to choose from at login.
It’s not published yet — I’m finishing some details before releasing the first version. But I wanted to tell the story before that.
The problem with alternatives
Before building something, it’s always worth seeing what exists. In my case, the options were:
Commercial managers (1Password, Bitwarden, Dashlane): good products, but oriented toward web passwords. For a developer managing dozens of API keys with fields like endpoint, region, scope or environment, the experience is clunky. And they depend on the cloud.
Infrastructure solutions (HashiCorp Vault, AWS Secrets Manager): excellent for teams and production, completely oversized for an individual developer who wants to store their keys securely during development.
System environment variables: valid, but no UI, no organization and no encryption at rest.
What I wanted was simple: a desktop app, local, encrypted, built for developers. I didn’t find exactly that, so I built it.
The decisions that matter
Building something security-related forces you to make decisions that are optional in other projects. Here are the ones that impacted me the most.
One mandatory dependency, the rest optional
The first decision was philosophical: keep the stack to the absolute minimum. Ciphie has one mandatory dependency — cryptography, the Python Cryptographic Authority package. Everything else it might need (QR codes for TOTP, SMS via Twilio, Touch ID on macOS) are optional extras installed only if you want them:
pip install ciphie # just the essentialspip install ciphie[qr] # adds QR codes for app-based 2FApip install ciphie[sms] # adds 2FA via SMS through Twiliopip install ciphie[all] # everything togetherThe UI, database and password hashing use modules that ship with Python: tkinter, sqlite3, hashlib. Every additional dependency is an attack surface. In a security project, that weighs more than the convenience of an extra library.
Real encryption, not security theater
Each secret is encrypted with AES-256-GCM before being stored. It’s not just an algorithm choice — it’s a mode that matters.
GCM (Galois/Counter Mode) does two things: it encrypts the data and authenticates it. If someone modifies the database file directly, decryption doesn’t silently return garbage: it fails with an explicit error. That’s the difference between a system that detects tampering and one that doesn’t.
def encrypt(value: str) -> str: key = _get_aes_key() nonce = os.urandom(12) # 96 bits — recommended size for GCM aesgcm = AESGCM(key) encrypted = aesgcm.encrypt(nonce, value.encode("utf-8"), None) return base64.urlsafe_b64encode(nonce + encrypted).decode("utf-8")The other thing that matters is the nonce: 12 randomly generated bytes for each individual encryption. Without it, two secrets with the same content produce the same encrypted result — and that breaks the security of the entire scheme. With the random nonce, each encryption is unique even if the content is identical.
Passwords are never stored
User passwords are hashed with PBKDF2-HMAC-SHA256 with 310,000 iterations — OWASP’s recommendation for 2023. Each extra iteration multiplies the time it takes an attacker to try passwords in an offline attack. With this configuration, a modern GPU would take months to brute-force, not seconds.
_ITERATIONS = 310_000
def _hash_password(password: str) -> str: salt = os.urandom(16) key = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, _ITERATIONS, dklen=64 ) return salt.hex() + ":" + key.hex()A detail I almost overlooked: hash comparison uses hmac.compare_digest instead of Python’s == operator. The difference is that == can terminate early if the first bytes don’t match, which allows timing measurement and leaking information about the real hash. compare_digest always takes exactly the same time, regardless of the result.
# ❌ vulnerable to timing attacksif calculated_hash == stored_hash:
# ✅ constant time, regardless of how many bytes matchif hmac.compare_digest(calculated_hash, stored_hash):SQLite is enough
I didn’t need a database server. SQLite runs in-process, the file lives locally with restricted permissions — only the owner can read it —, and it’s more than enough for this use case. Sometimes the right solution is the simplest one.
What surprises you when building something security-related
There’s a huge difference between knowing something exists and having to implement it correctly.
Timing attacks are a good example. I knew they existed. But it was different to have to think: “if I use == here, am I leaking information?” and have to find the correct alternative. Security is full of those moments: small details with large consequences.
Same with nonces. Or with the difference between authenticated and unauthenticated encryption. Or with the number of hash iterations. None of those details appear in an introductory tutorial, but all of them matter in production.
Building Ciphie was, among other things, an exercise in going beyond the surface of each concept.
What’s next
Before publishing the first version, I’m finishing some adjustments. When it’s ready, it will be available on GitHub under MIT license. One line to install, another to open it.
If you manage secrets as a developer and are interested in trying it when it’s available, follow me here — I’ll publish the launch as soon as it’s ready.
And if you have a keys.txt on your desktop, delete it now. Don’t wait for Ciphie.
Ciphie is built with Python 3.11+, SQLite, Tkinter and the cryptography library. The code will be available on GitHub under MIT license.