Criptografia at rest com a stdlib do Go

Quero guardar um dado no disco de forma que, mesmo que o arquivo vaze, ele só seja legível com a senha e que qualquer adulteração seja detectada.

Para isso vamos proteger os dados criptografando ele “at rest” o que basicamente significa que os dados estão protegidos quando são salvos no disco, e só podem ser acessados por quem tem a senha correta.

Dá para fazer isso só com a biblioteca padrão:

  • PBKDF2 para derivar a chave a partir da senha
  • AES-256-GCM para cifrar e autenticar.

Desde o Go 1.24 o crypto/pbkdf2 está na stdlib, então sem dependência externa.

A senha não vira chave direto. Ela passa pelo PBKDF2 com um salt aleatório e muitas iterações, isso encarece o ataque de força bruta. O AES-GCM é um modo autenticado: cifra e gera uma tag de integridade. Se alguém editar o arquivo, a verificação falha.

O arquivo final tem este layout, onde o salt vai junto (ele não é segredo, só precisa ser único) e o magic entra como dado adicional (AAD), sendo também autenticado:

magic | salt(16) | nonce(12) | ciphertext+tag

A chave sai da senha e vira um AES-256-GCM:

const (
	saltLen = 16
	keyLen  = 32      // AES-256
	iter    = 600_000 // PBKDF2 iterations
)

func newGCM(pass string, salt []byte) (cipher.AEAD, error) {
	key, err := pbkdf2.Key(
        sha256.New,
        pass,
        salt,
        iter,
        keyLen)
	if err != nil {
		return nil, err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	return cipher.NewGCM(block)
}

Cifrar é gerar salt e nonce aleatórios e montar o blob:

func encrypt(pass string, plain []byte) ([]byte, error) {
	salt := make([]byte, saltLen)
	if _, err := rand.Read(salt); err != nil {
		return nil, err
	}
	gcm, err := newGCM(pass, salt)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}
	ct := gcm.Seal(nil, nonce, plain, []byte(magic))

	out := append([]byte(magic), salt...)
	out = append(out, nonce...)
	return append(out, ct...), nil
}

O gcm.Open falha se a senha estiver errada ou se o arquivo foi alterado. As duas coisas dão o mesmo erro, e isso é proposital:

func decrypt(pass string, blob []byte) ([]byte, error) {
	if len(blob) < len(magic)+saltLen+12 || string(blob[:len(magic)]) != magic {
		return nil, fmt.Errorf("invalid format")
	}
	p := blob[len(magic):]
	salt, rest := p[:saltLen], p[saltLen:]
	gcm, err := newGCM(pass, salt)
	if err != nil {
		return nil, err
	}
	ns := gcm.NonceSize()
	if len(rest) < ns {
		return nil, fmt.Errorf("invalid format")
	}
	nonce, ct := rest[:ns], rest[ns:]
	pt, err := gcm.Open(nil, nonce, ct, []byte(magic))
	if err != nil {
		return nil, fmt.Errorf("wrong password (or tampered file)")
	}
	return pt, nil
}

Para testar:

export PASSWORD="my secret password"
go run . encrypt "sensitive data" > vault.bin
hexdump -C vault.bin
go run . decrypt < vault.bin

O hexdump -C mostra só ruído depois do magic.

Com crypto/pbkdf2 + crypto/aes + crypto/cipher você cifra dados “at rest” com zero dependências.

código fonte completo

Cesar Gimenes

Última modificação
Tags: