Um cofre cifrado em Go

Vamos juntar as três peças anteriores:

Um banco SQLite que vive na RAM e, no disco existe apenas como um blob criptografado.

O banco roda em :memory:. Ao sair, tiramos o snapshot dos bytes do banco, criptografamos com a senha e gravamos. Ao entrar, lemos o blob, decriptamos com a senha e fazemos o deserialize de volta para a RAM.

Aqui a proteção dos dados depende da força da senha, e do algoritmo de criptografia (aqui, AES-GCM). Precisa tomar cuidado com o tamanho do banco porque fazer o dump, criptografar e gravar tudo de uma vez pode ser lento. Mas é muito bom para salvar dados sensíveis, como chaves de API, tokens, etc.

O fluxo principal decide entre abrir um cofre existente ou criar um novo:

exists := fileExists(*vault)
pass, err := askPassphrase(!exists) // a new vault asks for confirmation
if err != nil {
	fatal(err)
}

db, _ := sql.Open("sqlite", ":memory:")
db.SetMaxOpenConns(1)
mustExec(db, `PRAGMA temp_store=MEMORY;`)

if exists {
	blob, _, err := readVault(*vault, pass)
	if err != nil {
		fatal(err) // wrong password or tampered: abort WITHOUT overwriting
	}
	restore(db, blob)
	list(db)
	return
}
// first time: create, serialize, encrypt and seal

Dois pontos importantes:

  • Nunca sobrescrever em senha errada, se o decrypt falha, abortamos antes de gravar qualquer coisa.
  • Escrita atômica, gravando num temporário e renomeando, para que uma queda no meio da escrita não deixe um cofre pela metade.
func seal(db *sql.DB, path, pass string) error {
	plain, err := snapshot(db)
	if err != nil {
		return err
	}
	blob, err := encrypt(pass, plain)
	if err != nil {
		return err
	}
	tmp, err := os.CreateTemp("", ".vault-*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	if _, werr := tmp.Write(blob); werr != nil {
		_ = tmp.Close()
		_ = os.Remove(tmpName)
		return werr
	}
	if cerr := tmp.Close(); cerr != nil {
		_ = os.Remove(tmpName)
		return cerr
	}
	if cerr := os.Chmod(tmpName, 0o600); cerr != nil {
		_ = os.Remove(tmpName)
		return cerr
	}
	return os.Rename(tmpName, path)
}

Para testar:

go run .                      # create, ask for the password twice, seal
file vault.blob               # not a SQLite file
hexdump -C vault.blob | head  # nothing readable
go run .                      # ask for the password, open and list

Isso protege o conteúdo do banco at rest: sem a senha, o arquivo é ruído, e qualquer adulteração é detectada pelo GCM.

Claro que os dados são protegidos apenas no disco, não protege contra um atacante que leia a memória do processo.

código fonte completo

Cesar Gimenes

Última modificação
Tags: