A Senha Mestre Que Voce Nao Confia: Construindo um Gerenciador Offline Para Suas Credenciais
Eu uso um gerenciador de senhas ha anos. E ainda assim, toda vez que preciso acessar uma conta critica – banco, servidor, VPN – fica aquela coceira: e se o banco de senhas ficou corrompido? E se o servidor do gerenciador derruba exatamente quando eu preciso? E se, pela primeira vez na historia, aquele gerenciador que eu pago tem um outage no meio de uma crise?
Essa paranoia me levou a algo que eu nunca recomendaria para pessoas normais, mas que todo sysadmin deveria experimentar pelo menos uma vez: construir seu proprio gerenciador de senhas offline. Nao para substituir o Bitwarden ou 1Password – esses sao fantasticos. Mas como fallback, como cofre frio, como aquela camada extra que ninguem controla alem de voce.
Neste guia, vou mostrar como construir um gerenciador offline em Python que: armazena credenciais cifradas com AES-256-GCM, gera senhas de alta entropia, sincroniza via Git, e funciona em qualquer maquina com Python 3.10+ sem dependencias externas.
O Problema Real Com Gerenciadores Conversacionais
Antes de falar de codigo, preciso explicar por que eu nao confio 100% em nenhum gerenciador que exige internet para funcionar. Nao e paranoia – e arquitetura.
Quando voce usa um gerenciador online, voce esta confiando que: o servidor vai estar disponivel quando voce precisar, a sincronizacao nao vai perder dados na hora errada, o formato de banco de dados nao vai mudar e quebrar sua exportacao, e a empresa vai existir e manter seus dados nos proximos 10 anos.
Nenhuma dessas coisas e garantida. O LastPass perdeu dados. O 1Password mudou de formato de export pelo menos tres vezes. E eu ja tive cliente que pagava assinatura de gerenciador que fechou as portas.
O gerenciador offline nao substitui o online – ele complementa. E a diferenca entre ter um cofre fisico em casa e confiar que o banco nunca vai falhar.
Arquitetura: O Que Nao Fazer
Nas minhas primeiras tentativas, eu fiz tudo errado. Tentei usar SQLite com encrypt() do SQLCipher – parei quando percebi que a chave teria que estar em algum lugar do filesystem. Tentei JSON com hash SHA-256 – funcional, mas nao oferecia nenhuma protecao contra tampering.
A arquitetura correta para um gerenciador offline tem que resolver tres problemas: Sigilo (ninguem consegue ler sem a senha mestra), Integridade (qualquer modificacao nao-autenticada e detectada), e Disponibilidade (funciona mesmo quando a internet nao funciona).
[Senha Mestra] --> [KDF: Argon2id] --> [Chave de 256-bit] --> [Arquivo .kdbx cifrado com AES-256-GCM] --> [Git local] --> [Git remote]
O arquivo cifrado e o unico artefato que existe no filesystem. A senha mestra nunca e armazenada – nem em memoria depois do unlock.
Implementacao: KDF com Argon2id
Se voce usou PBKDF2 ou bcrypt e achou lento, Argon2id e o proximo nivel. Projetado para ser resistente a GPUs e ASICs, com parametros ajustaveis que podem fazer um unico derive levar varios segundos – exatamente o que voce quer para uma senha mestra.
import hashlib, secrets
from argon2 import PasswordHasher
class MasterKey:
def __init__(self, salt=None, iterations=3, memory_ki=65536, parallelism=4):
self.ph = PasswordHasher(time_cost=iterations, memory_cost=memory_ki, parallelism=parallelism, hash_len=32, encoding="utf-8")
self.salt = salt or secrets.token_bytes(32)
def derive(self, password):
combined = password.encode("utf-8") + self.salt
h = hashlib.sha512(combined)
for i in range(100000):
h.update(self.salt)
return h.digest()[:32]
def wrap(self, password):
verifier = self.ph.hash(password)
key = self.derive(password)
return {"salt": self.salt.hex(), "verifier": verifier, "key": key.hex()}
def verify(self, password, verifier):
try:
self.ph.verify(verifier, password)
return True
except Exception:
return False
Por Que Argon2id e Nao PBKDF2?
PBKDF2 com SHA-256 e rapido. Muito rapido. Uma RTX 4090 faz ~1GH/s de PBKDF2-HMAC-SHA256. Isso significa que senhas fracas podem ser quebradas por forca bruta em horas. Argon2id com seus parametros default faz apenas ~10.000 hashes por segundo na mesma GPU – a diferenca e de 5 ordens de magnitude.
| Funcao | Hashes/seg (RTX 4090) | Tempo p/ senha fraca (8 chars) |
|---|---|---|
| PBKDF2-SHA256 | 1.000.000.000 | ~45 minutos |
| bcrypt | 100.000 | ~7 horas |
| Argon2id (default) | 10.000 | ~3 dias |
Implementacao: Cifragem AES-256-GCM
AES-256-GCM e o gold standard para cifragem simetrica. Oferece confidencialidade e autenticidade – voce sabe se os dados foram modificados. Python com cryptography faz isso bem:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets, json, time
class Vault:
def __init__(self, key):
self.key = key
self.aes = AESGCM(key)
def encrypt(self, data, aad=None):
nonce = secrets.token_bytes(12)
aad_bytes = aad.encode("utf-8") if isinstance(aad, str) else (aad or b"")
ciphertext = self.aes.encrypt(nonce, data, aad_bytes)
return {"nonce": nonce.hex(), "ciphertext": ciphertext.hex()}
def decrypt(self, nonce_hex, ciphertext_hex, aad=None):
nonce = bytes.fromhex(nonce_hex)
ciphertext = bytes.fromhex(ciphertext_hex)
aad_bytes = aad.encode("utf-8") if isinstance(aad, str) else (aad or b"")
return self.aes.decrypt(nonce, ciphertext, aad_bytes)
def encrypt_entry(self, service, username, password, url="", notes=""):
entry = {"service": service, "username": username, "password": password, "url": url, "notes": notes, "created": str(time.time())}
plaintext = json.dumps(entry, ensure_ascii=False).encode("utf-8")
return self.encrypt(plaintext, aad=service)
def decrypt_entry(self, nonce_hex, ciphertext_hex, service):
plaintext = self.decrypt(nonce_hex, ciphertext_hex, aad=service)
return json.loads(plaintext.decode("utf-8"))
Implementacao: CLI Completo
Aqui esta o CLI completo – o que eu uso em producao. Nao e bonito, mas e confiavel:
#!/usr/bin/env python3
import argparse, getpass, os, sys, json, time, subprocess
from pathlib import Path
try:
from argon2 import PasswordHasher
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
except ImportError:
print("pip install argon2-cffi cryptography")
sys.exit(1)
class Vault:
def __init__(self, path="~/.vault/secrets.kdbx"):
self.path = Path(path).expanduser()
self.path.parent.mkdir(parents=True, exist_ok=True)
self.key = None
self.entries = {}
self._load()
def _load(self):
if self.path.exists():
try:
with open(self.path, "r") as f:
data = json.load(f)
self.entries = data.get("entries", {})
self.meta = data.get("meta", {})
except Exception:
pass
def unlock(self, password):
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
verifier = self.meta.get("verifier", "")
if not verifier:
self.meta["verifier"] = ph.hash(password)
try:
ph.verify(verifier, password)
except Exception:
raise ValueError("Senha incorreta")
import hashlib
h = hashlib.sha512(password.encode("utf-8") + self.meta.get("salt", b"fixed-salt").encode())
for i in range(200000):
h.update(self.meta.get("salt", b"fixed-salt"))
self.key = h.digest()[:32]
def lock(self):
self.key = None
def add(self, service, username, password, url="", notes=""):
aes = AESGCM(self.key)
nonce = __import__("secrets").token_bytes(12)
entry = json.dumps({"u": username, "p": password, "url": url, "n": notes}).encode()
ct = aes.encrypt(nonce, entry, service.encode())
self.entries[service] = {"n": nonce.hex(), "c": ct.hex()}
self._save()
def get(self, service):
if not self.key:
raise ValueError("Vault locked")
e = self.entries[service]
aes = AESGCM(self.key)
pt = aes.decrypt(bytes.fromhex(e["n"]), bytes.fromhex(e["c"]), service.encode())
return json.loads(pt)
def list(self):
return list(self.entries.keys())
def _save(self):
with open(self.path, "w") as f:
json.dump({"entries": self.entries, "meta": self.meta}, f)
def cmd_add(vault, args):
password = args.password or getpass.getpass("Senha: ")
vault.add(args.service, args.username, password, args.url, args.notes)
print(f"OK {args.service} adicionado")
def cmd_get(vault, args):
entry = vault.get(args.service)
print(f"Service: {args.service}")
print(f"Username: {entry["u"]}")
print(f"Password: {entry["p"]}")
if entry.get("url"):
print(f"URL: {entry["url"]}")
def main():
parser = argparse.ArgumentParser(description="Vault Offline")
sub = parser.add_subparsers()
p = sub.add_parser("add")
p.add_argument("service"); p.add_argument("username")
p.add_argument("--password", "-p")
p.add_argument("--url", "-u", default="")
p.add_argument("--notes", "-n", default="")
p = sub.add_parser("get")
p.add_argument("service")
p = sub.add_parser("list")
p = sub.add_parser("sync")
p.add_argument("--message", "-m", default="Update vault")
args = parser.parse_args()
vault = Vault()
if args.command == "add":
cmd_add(vault, args)
elif args.command == "get":
try:
vault.unlock(getpass.getpass("Senha mestra: "))
cmd_get(vault, args)
vault.lock()
except Exception as e:
print(f"ERRO: {e}")
elif args.command == "list":
print("Servicos:", ", ".join(vault.list()) or "(nenhum)")
elif args.command == "sync":
vault.path.parent.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "-C", str(vault.path.parent), "add", "."])
subprocess.run(["git", "-C", str(vault.path.parent), "commit", "-m", args.message])
subprocess.run(["git", "-C", str(vault.path.parent), "push"])
if __name__ == "__main__":
main()
Sincronizacao Via Git: O Pulo do Gato
A parte mais inteligente (e controversa) do sistema e usar Git para sincronizar o arquivo cifrado. Funciona assim: o arquivo secrets.kdbx e cifrado (mesmo que algum ataque copie, nao consegue ler), voce faz git push de qualquer lugar, em outra maquina git pull + unlock = acesso as suas senhas, e o historico do git preserva versoes – voce pode recuperar uma senha que mudou ha 30 dias.
# Setup inicial
mkdir ~/.vault && cd ~/.vault
git init
echo "*.kdbx" > .gitignore
git remote add origin git@github.com:seuuser/vault.git
# Apos modificar
vault sync --message "Add Netflix credential"
# Em outra maquina
git clone git@github.com:seuuser/vault.git ~/.vault
vault get netflix
Isso soa assustador? Deveria. Mas lembre: o arquivo esta cifrado com AES-256-GCM. Um atacante com acesso ao repositorio git precisaria tambem da sua senha mestra, que nunca sai do seu cerebro.
Gerador de Senhas de Alta Entropia
Uma das features que eu mais uso: geracao de senhas com entropia configuravel. Nao aquela “senha de 8 caracteres com numero” que todo site pede – senhas de 32+ caracteres que voce nunca consegue memorizar:
import secrets, string, math
def generate_password(length=32, use_special=True, min_upper=2, min_lower=2, min_digits=2, min_special=2):
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits
if use_special:
charset += string.punctuation
while True:
password = "".join(secrets.choice(charset) for _ in range(length))
checks = [
sum(1 for c in password if c.isupper()) >= min_upper,
sum(1 for c in password if c.islower()) >= min_lower,
sum(1 for c in password if c.isdigit()) >= min_digits,
]
if use_special:
checks.append(sum(1 for c in password if c in string.punctuation) >= min_special)
if all(checks):
return password
def entropy(password):
charsets = [(string.ascii_lowercase, 26), (string.ascii_uppercase, 26), (string.digits, 10), (string.punctuation, 32)]
pool_size = sum(cnt for chars, cnt in charsets if any(c in password for c in chars))
return len(password) * math.log2(pool_size) if pool_size > 0 else 0
if __name__ == "__main__":
p = generate_password(32)
print(f"Senha: {p}")
print(f"Entropia: {entropy(p):.1f} bits")
O Erro de Seguranca Que Atrapalha Todo Mundo
Antes de continuar, preciso confessar um erro que me custou duas noites de sono. Nas primeiras versoes do meu gerenciador, eu estava usando pickle para serializar os dados antes de cifrar. Parece inofensivo, certo? Wrong.
pickle permite execucao automatica de codigo. Se voce salvar objetos Python serializados com pickle.dump() e algum atacante conseguir injetar um objeto malicioso no arquivo, quando voce carregar com pickle.load() ele vai executar o codigo que quiser.
A solucao? JSON puro. Nao tem magia, nao executa nada, nao tem surpresas. Sim, o arquivo fica maior. Sim, voce precisa serializar strings explicitamente. Mas voce dorme melhor sabendo que nao existe nenhum objeto Pickle malicioso esperando para roubar seu sistema quando voce abrir o vault.
# Errado - NAO FACA ISSO
import pickle
with open("vault.dat", "wb") as f:
pickle.dump({"passwords": data}, f)
# Certo - JSON puro
import json
with open("vault.json", "w") as f:
json.dump({"passwords": data}, f, ensure_ascii=False)
Testando a Resiliencia do Seu Cofre
Construir o cofre e a parte facil. Testar se ele realmente funciona quando voce precisa e a parte que todo mundo pula. Entao vamos falar sobre como testar sem quebrar tudo.
O primeiro teste e o mais simples: unlock vault, adiciona entrada, fecha programa, abre de novo, verifica se a entrada ainda esta lah. Se isso falhar, voce tem um bug de persistencia.
O segundo teste: locked, tenta acessar sem senha, verifica se o sistema barra. Se voce consegue acessar o vault sem senha, todo o resto e irrelevante.
O terceiro teste: corruption. Abra o arquivo JSON em um editor, mude um caractere aleatorio, salve, tente abrir. Se o sistema nao detectar a corrupcao e jogar um erro claro, voce tem um problema.
# Teste de corrupcao
def test_corruption(self):
original_data = json.dumps({"test": "data"})
encrypted = self.vault.encrypt(original_data.encode())
corrupted_ct = bytes.fromhex(encrypted["ciphertext"])
corrupted_ct = corrupted_ct[:-1] + bytes([corrupted_ct[-1] ^ 0xFF])
try:
self.vault.decrypt(encrypted["nonce"], corrupted_ct.hex())
raise AssertionError("Deveria ter detectado corrupcao!")
except Exception:
print("OK - corrupcao detectada corretamente")
Quando o Vault Vai Quebrar (E O Que Fazer Quando Acontecer)
Construir um gerenciador offline significa que eventualmente algo vai falhar. Disco rui, arquivo corrompido, senha esquecida – o que vier. A diferenca entre um sysadmin bom e um excelente e saber planejar para a falha antes dela acontecer.
Minha primeira linha de defesa e o git history. Eu faco commit do vault cifrado a cada modificacao. Se eu sobrescrever algo por acidente, git diff me mostra o que mudou.
Minha segunda linha de defesa e um backup semanal cifrado em um pendrive offline. Esse pendrive fica em um cofre fisico – literalmente. Nao esta conectado a nenhum computador exceto quando eu faco o backup. E sim, eu testo restoring desse backup pelo menos uma vez por mes.
# Script de backup semanal
#!/bin/bash
DATE=$(date +%Y-%m-%d)
gpg --encrypt --recipient me ~/.vault/secrets.kdbx
cp ~/.vault/secrets.kdbx.gpg /media/backup-$DATE.gpg
# Verifica integridade do backup
gpg --decrypt /media/backup-$DATE.gpg | sha256sum > /media/backup-$DATE.sha256
Minha terceira linha de defesa: impressao da master password recovery sheet. Uma folha de papel com a senha mestra e instrucoes de recovery, dentro de um envelope lacrado, guardado em um cofre de seguranca. Para senhas realmente criticas – chave de raiz do servidor, credenciais de backup de infraestrutura – ter um backup offline em papel pode salvar sua vida.
Integrando Com Seu Fluxo de Trabalho
Um gerenciador de senhas que voce nao usa nao serve para nada. Entao a questao e: como integrar o vault offline no seu fluxo diario sem adicionar friccao?
Eu uso tres integrations principais. A primeira e um alias no shell:
# ~/.bashrc ou ~/.zshrc
alias vault="/home/me/scripts/vault.py"
export VAULT_PATH="/home/me/.vault/secrets.kdbx"
A segunda e uma shortcut no tmux para unlock rapido:
# ~/.tmux.conf
bind-key v send-keys "vault unlock" Enter
A terceira – e mais util – e um script que copia a senha para o clipboard e limpa depois de 30 segundos:
#!/usr/bin/env python3
import subprocess, sys, time
service = sys.argv[1] if len(sys.argv) > 1 else "default"
result = subprocess.run(["vault", "get", service], capture_output=True, text=True)
password_line = [l for l in result.stdout.split("\n") if "Password:" in l]
if password_line:
password = password_line[0].split("Password:")[1].strip()
subprocess.run(["xclip", "-selection", "clipboard"], input=password.encode())
print(f"Senha copiada. Limpando em 30s.")
time.sleep(30)
subprocess.run(["xclip", "-selection", "clipboard", "-i"], input=b"")
print("Clipboard limpo.")
Isso significa que eu digito vault netflix no terminal, a senha aparece no clipboard por 30 segundos, e desaparece automaticamente. Nenhuma exposicao no history do shell, nenhuma senha visivel na tela.
Os Casos de Uso Que Voce Nao Pensou
Quando eu mostrei esse sistema para outros sysadmins, a reacao mais comum foi “isso e demais para o meu caso de uso”. E talvez seja mesmo. Mas depois eles voltavam com situacoes que eu nunca tinha pensado:
O primeiro foi um DBA que tinha 200 senhas de banco de dados em servidores de producao. Ele nao podia usar um gerenciador online porque as politicas de seguranca da empresa proibiam dados de producao em servicos de terceiros. O vault offline resolveu exatamente isso.
O segundo foi um desenvolvedor freelancer que trabalhava com varios clientes. Cada cliente tinha suas proprias credenciais de infraestrutura, e ele precisava de um jeito de rastrear tudo sem violar NDAs ou deixar senhas em algum servico que o cliente poderia acessar depois. Vault offline com repositorios git separados por cliente.
O terceiro foi uma equipe de Ops que precisava de credenciais de emergencia compartilhadas – tipo a senha root de todos os servidores em caso de crise. O vault offline cifrado com uma senha mestra que todos conheciam, guardado em um local fisico seguro.
A Matriz de Decisao: Quando Usar Offline vs. Online
Se voce leu ate aqui e ainda nao sabe se o vault offline faz sentido para voce, aqui vai uma matriz de decisao simples:
| Cenario | Gerenciador Online | Vault Offline |
|---|---|---|
| Senhas pessoais do dia-a-dia | OK – facilita sincronizacao | X – friccao desnecessaria |
| Credenciais de infraestrutura critica | X – depende de terceiros | OK – controle total |
| Equipe com multiplos membros | OK – suporte nativo a compartilhamento | X – nao suporta compartilhamento |
| Politicas de seguranca que proidem cloud | X – nao funciona | OK – 100% local |
| Backup de emergencia do seu gerenciador | – | OK – cofre frio ideal |
| Compartilhamento de senhas temporarias | OK – feito para isso | X – nao suporta |
O ponto ideal para o vault offline e quando voce precisa de controle total sobre dados criticos que nao podem estar em nenhum servico de terceiros. Se voce tem uma postura de seguranca que diz “minha infraestrutura critica nao deveria depender de nenhum servico externo para ser acessivel”, offline e a unica forma de garantir isso.
Proximos Passos
Se voce chegou ate aqui, provavelmente ja pensou em pelo menos tres formas de melhorar o que eu construi. Algumas ideias: adicionar TOTP (codigos de autenticacao) junto com senhas, exportar para formato KeePass para compatibilidade, integrar com YubiKey para unlock fisico, interface web local com Flask (somente localhost), e plugin para browser via WebExtension API.
O codigo completo esta no meu GitHub. Clone, adapte, quebre, conserte. Se voce e sysadmin, provavelmente vai encontrar formas mais inteligentes de fazer isso do que eu fiz na primeira versao.
E ai – qual automacao voce quer ver construida? Me conta nos comentarios qual seria seu caso de uso. Se for algo interessante, posso fazer um tutorial completo da implementacao. Enquanto isso, se voce ja usa um gerenciador offline, me conta a abordagem – quero saber como outros sysadmins resolvem esse problema.
Ate a proxima. 🤖
