cover/Elixir.WalletAuth.Credentials.CredentialPolicy.html

1 defmodule WalletAuth.Credentials.CredentialPolicy do
2 @moduledoc """
3 Password hashing and verification policy.
4
5 Per ADR 0006: credentials must be stored as hashes, never plaintext.
6
7 Implementation:
8 - Uses PBKDF2-SHA256 via `:crypto.pbkdf2_hmac/5` (available in OTP).
9 - Salt: 16 bytes of CSPRNG, stored alongside hash.
10 - Format: `"pbkdf2_sha256$<iterations>$<salt_hex>$<hash_hex>"`.
11
12 Production note: for higher-security deployments replace with Argon2 or bcrypt
13 by implementing the pluggable hasher below. The hash/3 and verify/2 functions
14 accept an optional `hasher_module` for that purpose.
15
16 Timing-safe comparison is used for all verification to prevent timing attacks.
17 """
18
19 @iterations 100_000
20 @hash_bytes 32
21 @salt_bytes 16
22 @algorithm :sha256
23
24 @type hash_string :: String.t()
25
26 @doc """
27 Hashes a password string. Returns an opaque hash string suitable for storage.
28 """
29 @spec hash(password :: String.t()) :: hash_string()
30 def hash(password) when is_binary(password) do
31 2 salt = :crypto.strong_rand_bytes(@salt_bytes)
32 2 hash_bytes = derive(password, salt)
33
34 2 "pbkdf2_sha256$#{@iterations}$#{Base.encode16(salt, case: :lower)}$#{Base.encode16(hash_bytes, case: :lower)}"
35 end
36
37 @doc """
38 Verifies a plaintext password against a stored hash string.
39
40 Returns `true` or `false`. Always runs to completion (no early exit) to
41 prevent timing oracle attacks.
42 """
43 @spec verify(password :: String.t(), stored_hash :: hash_string()) :: boolean()
44 def verify(password, stored_hash) when is_binary(password) and is_binary(stored_hash) do
45 2 with ["pbkdf2_sha256", iter_str, salt_hex, hash_hex] <- String.split(stored_hash, "$"),
46 2 {iterations, ""} <- Integer.parse(iter_str),
47 2 {:ok, salt} <- Base.decode16(salt_hex, case: :lower),
48 2 {:ok, expected_hash} <- Base.decode16(hash_hex, case: :lower) do
49 2 derived = :crypto.pbkdf2_hmac(@algorithm, password, salt, iterations, @hash_bytes)
50 2 Plug.Crypto.secure_compare(derived, expected_hash)
51 else
52 _ -> false
53 end
54 end
55
56
:-(
def verify(_, _), do: false
57
58 defp derive(password, salt) do
59 2 :crypto.pbkdf2_hmac(@algorithm, password, salt, @iterations, @hash_bytes)
60 end
61 end
Line Hits Source