forked from OWASP/cornucopia
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbinary.ex
More file actions
89 lines (77 loc) · 2.59 KB
/
Copy pathbinary.ex
File metadata and controls
89 lines (77 loc) · 2.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
defmodule Copi.Encrypted.Binary do
@moduledoc """
Custom Ecto type that encrypts/decrypts values using AES-256-GCM.
Key is loaded from COPI_ENCRYPTION_KEY env var first, then application config.
Stored as binary in the database.
"""
use Ecto.Type
@magic_prefix "ENC1"
@iv_bytes 12
@tag_bytes 16
@impl Ecto.Type
def type, do: :binary
@impl Ecto.Type
def cast(value) when is_binary(value), do: {:ok, value}
def cast(_), do: :error
@impl Ecto.Type
def dump(nil), do: {:ok, nil}
def dump(value) when is_binary(value) do
case encrypt(value) do
{:ok, blob} -> {:ok, blob}
# coveralls-ignore-next-line
{:error, reason} -> raise "Copi.Encrypted.Binary dump/1 failed: #{reason}"
end
end
# coveralls-ignore-next-line
def dump(_), do: :error
@impl Ecto.Type
def load(nil), do: {:ok, nil}
def load(value) when is_binary(value) do
case decrypt(value) do
{:ok, plaintext} -> {:ok, plaintext}
{:error, :not_encrypted} -> {:ok, value}
# coveralls-ignore-next-line
{:error, reason} -> raise "Copi.Encrypted.Binary load/1 failed: #{reason}"
end
end
# coveralls-ignore-next-line
def load(_), do: :error
def encrypt(plaintext) when is_binary(plaintext) do
with {:ok, key} <- fetch_key() do
iv = :crypto.strong_rand_bytes(@iv_bytes)
{ciphertext, tag} =
:crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, plaintext, @magic_prefix, true)
blob = @magic_prefix <> iv <> tag <> ciphertext
{:ok, blob}
end
end
def decrypt(blob) when is_binary(blob) do
case blob do
<<@magic_prefix, iv::binary-size(@iv_bytes), tag::binary-size(@tag_bytes),
ciphertext::binary>> ->
with {:ok, key} <- fetch_key() do
case :crypto.crypto_one_time_aead(
:aes_256_gcm, key, iv, ciphertext, @magic_prefix, tag, false
) do
# coveralls-ignore-next-line
:error -> {:error, "AES-GCM authentication failed"}
plaintext -> {:ok, plaintext}
end
end
_ ->
{:error, :not_encrypted}
end
end
defp fetch_key do
raw =
System.get_env("COPI_ENCRYPTION_KEY") ||
Application.get_env(:copi, :encryption_key) ||
raise "COPI_ENCRYPTION_KEY is not set. Please see: https://github.com/OWASP/cornucopia/blob/master/copi.owasp.org/SECURITY.md#encryption-key-setup"
key = Base.decode64!(String.trim(raw))
if byte_size(key) != 32 do
raise ArgumentError,
"COPI_ENCRYPTION_KEY must decode to exactly 32 bytes, got #{byte_size(key)}"
end
{:ok, key}
end
end