• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

danielberkompas / cloak / b9e89d6b-4932-4f57-a97b-d8794b1d9249

pending completion
b9e89d6b-4932-4f57-a97b-d8794b1d9249

Pull #123

semaphore

Merge 8b8e156e2 into 404727b1b
Pull Request #123: allow other genserver return values

98 of 100 relevant lines covered (98.0%)

10.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.15
/lib/cloak/vault.ex
1
defmodule Cloak.Vault do
2
  @moduledoc """
3
  Encrypts and decrypts data, using a configured cipher.
4

5
  ## Create Your Vault
6

7
  Define a module in your application that uses `Cloak.Vault`.
8

9
      defmodule MyApp.Vault do
10
        use Cloak.Vault, otp_app: :my_app
11
      end
12

13
  ## Configuration
14

15
  The `:otp_app` option should point to an OTP application that has the vault
16
  configuration.
17

18
  For example, the vault:
19

20
      defmodule MyApp.Vault do
21
        use Cloak.Vault, otp_app: :my_app
22
      end
23

24
  Could be configured with Mix configuration like so:
25

26
      config :my_app, MyApp.Vault,
27
        json_library: Jason,
28
        ciphers: [
29
          default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: <<...>>}
30
        ]
31

32
  The configuration options are:
33

34
  - `:json_library`: Used to convert data types like lists and maps into
35
    binary so that they can be encrypted. (Default: `Jason`)
36

37
  - :ciphers: a list of `Cloak.Cipher` modules the following format:
38

39
          {:label, {CipherModule, opts}}
40

41
    **The first configured cipher in the list is the default for encrypting
42
    all new data, regardless of its label.** This behaviour can be overridden
43
    on a field-by-field basis.
44

45
    The `opts` are specific to each cipher module. Check their
46
    codumentation for what each cipher requires.
47

48
      - `Cloak.Ciphers.AES.GCM`
49
      - `Cloak.Ciphers.AES.CTR`
50

51
  ### Runtime Configuration
52

53
  Because Vaults are GenServers, they can be configured at runtime using the
54
  `init/1` callback. This allows you to easily fetch values like environment
55
  variables in a reliable way.
56

57
  The configuration from the `:otp_app` is passed as the first argument to the
58
  callback, allowing you to append to or change it at will.
59

60
      defmodule MyApp.Vault do
61
        use Cloak.Vault, otp_app: :my_app
62

63
        @impl GenServer
64
        def init(config) do
65
          config =
66
            Keyword.put(config, :ciphers, [
67
              default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("CLOAK_KEY")}
68
            ])
69

70
          {:ok, config}
71
        end
72

73
        defp decode_env!(var) do
74
          var
75
          |> System.get_env()
76
          |> Base.decode64!()
77
        end
78
      end
79

80
  You can also pass configuration to vaults via `start_link/1`:
81

82
      MyApp.Vault.start_link(ciphers: [
83
        default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: key}
84
      ])
85

86
  ## Supervision
87

88
  Because Vaults are `GenServer`s, you'll need to add your vault to your
89
  supervision tree in `application.ex` or whichever supervisor you prefer.
90

91
      children = [
92
        MyApp.Vault
93
      ]
94

95
  If you want to pass in configuration values at runtime, you can do so:
96

97
      children = [
98
        {MyApp.Vault, ciphers: [...]}
99
      ]
100

101
  ## Usage
102

103
  You can use the vault directly by calling its functions.
104

105
      MyApp.Vault.encrypt("plaintext")
106
      # => {:ok, <<...>>}
107

108
      MyApp.Vault.decrypt(ciphertext)
109
      # => {:ok, "plaintext"}
110

111
  See the documented callbacks below for the functions you can call.
112

113
  ### Performance Notes
114

115
  Vaults are not bottlenecks. They simply store configuration in an ETS table
116
  named after the Vault, e.g. `MyApp.Vault.Config`. All encryption and
117
  decryption is performed in your local process, reading configuration from
118
  the vault's ETS table.
119
  """
120

121
  @type plaintext :: binary
122
  @type ciphertext :: binary
123
  @type label :: atom
124

125
  @doc """
126
  Encrypts a binary using the first configured cipher in the vault's
127
  configured `:ciphers` list.
128
  """
129
  @callback encrypt(plaintext) :: {:ok, ciphertext} | {:error, Exception.t()}
130

131
  @doc """
132
  Like `encrypt/1`, but raises any errors.
133
  """
134
  @callback encrypt!(plaintext) :: ciphertext | no_return
135

136
  @doc """
137
  Encrypts a binary using the vault's configured cipher with the
138
  corresponding label.
139
  """
140
  @callback encrypt(plaintext, label) :: {:ok, ciphertext} | {:error, Exception.t()}
141

142
  @doc """
143
  Like `encrypt/2`, but raises any errors.
144
  """
145
  @callback encrypt!(plaintext, label) :: ciphertext | no_return
146

147
  @doc """
148
  Decrypts a binary with the configured cipher that generated the binary.
149
  Automatically detects which cipher to use, based on the ciphertext.
150
  """
151
  @callback decrypt(ciphertext) :: {:ok, plaintext} | {:error, Exception.t()}
152

153
  @doc """
154
  Like `decrypt/1`, but raises any errors.
155
  """
156
  @callback decrypt!(ciphertext) :: plaintext | no_return
157

158
  @doc """
159
  The JSON library the vault uses to convert maps and lists into
160
  JSON binaries before encryption.
161
  """
162
  @callback json_library :: module
163

164
  defmacro __using__(opts) do
165
    otp_app = Keyword.fetch!(opts, :otp_app)
2✔
166

167
    quote location: :keep do
168
      use GenServer
169

170
      @behaviour Cloak.Vault
171
      @otp_app unquote(otp_app)
172
      @table_name :"#{__MODULE__}.Config"
173

174
      ###
175
      # GenServer
176
      ###
177

178
      def start_link(config \\ []) do
179
        # Merge passed in configuration with otp_app configuration
180
        app_config = Application.get_env(@otp_app, __MODULE__, [])
181
        config = Keyword.merge(app_config, config)
182

183
        case GenServer.start_link(__MODULE__, config, name: __MODULE__) do
184
          {:ok, pid} ->
185
            # Ensure that the configuration is saved
186
            GenServer.call(pid, :save_config, 10_000)
187
            # Return the pid
188
            {:ok, pid}
189

190
          other ->
191
            other
192
        end
193
      end
194

195
      # Users can override init/1 to customize the configuration
196
      # of the vault during startup
197
      @impl GenServer
198
      def init(config) do
199
        {:ok, config}
200
      end
201

202
      # Cache the results of the `init` configuration callback in
203
      # the application configuration for this Vault.
204
      @impl GenServer
205
      def handle_call(:save_config, _from, config) do
206
        Cloak.Vault.save_config(@table_name, config)
207
        {:reply, :ok, config}
208
      end
209

210
      # If a hot upgrade occurs, rerun the `init` callback to
211
      # refresh the configuration in case it changed
212
      @impl GenServer
213
      def code_change(_vsn, config, _extra) do
214
        config = init(config)
215
        Cloak.Vault.save_config(@table_name, config)
216
        {:ok, config}
217
      end
218

219
      ###
220
      # Encrypt/Decrypt functions
221
      ###
222

223
      @impl Cloak.Vault
224
      def encrypt(plaintext) do
225
        @table_name
226
        |> Cloak.Vault.read_config()
227
        |> Cloak.Vault.encrypt(plaintext)
228
      end
229

230
      @impl Cloak.Vault
231
      def encrypt!(plaintext) do
232
        @table_name
233
        |> Cloak.Vault.read_config()
234
        |> Cloak.Vault.encrypt!(plaintext)
235
      end
236

237
      @impl Cloak.Vault
238
      def encrypt(plaintext, label) do
239
        @table_name
240
        |> Cloak.Vault.read_config()
241
        |> Cloak.Vault.encrypt(plaintext, label)
242
      end
243

244
      @impl Cloak.Vault
245
      def encrypt!(plaintext, label) do
246
        @table_name
247
        |> Cloak.Vault.read_config()
248
        |> Cloak.Vault.encrypt!(plaintext, label)
249
      end
250

251
      @impl Cloak.Vault
252
      def decrypt(ciphertext) do
253
        @table_name
254
        |> Cloak.Vault.read_config()
255
        |> Cloak.Vault.decrypt(ciphertext)
256
      end
257

258
      @impl Cloak.Vault
259
      def decrypt!(ciphertext) do
260
        @table_name
261
        |> Cloak.Vault.read_config()
262
        |> Cloak.Vault.decrypt!(ciphertext)
263
      end
264

265
      @impl Cloak.Vault
266
      def json_library do
267
        @table_name
268
        |> Cloak.Vault.read_config()
269
        |> Keyword.get(:json_library, Jason)
270
      end
271

272
      defoverridable(Module.definitions_in(__MODULE__))
273
    end
274
  end
275

276
  @doc false
277
  def save_config(table_name, config) do
278
    if :ets.info(table_name) == :undefined do
6✔
279
      :ets.new(table_name, [:named_table, :protected])
6✔
280
    end
281

282
    :ets.insert(table_name, {:config, config})
6✔
283
  end
284

285
  @doc false
286
  def read_config(table_name) do
287
    case :ets.lookup(table_name, :config) do
24✔
288
      [{:config, config} | _] ->
289
        config
24✔
290

291
      _ ->
×
292
        :error
293
    end
294
  end
295

296
  @doc false
297
  def encrypt(config, plaintext) do
298
    with [{_label, {module, opts}} | _ciphers] <- config[:ciphers] do
7✔
299
      module.encrypt(plaintext, opts)
5✔
300
    else
301
      _ ->
302
        {:error, Cloak.InvalidConfig.exception("could not encrypt due to missing configuration")}
303
    end
304
  end
305

306
  @doc false
307
  def encrypt!(config, plaintext) do
308
    case encrypt(config, plaintext) do
4✔
309
      {:ok, ciphertext} ->
310
        ciphertext
3✔
311

312
      {:error, error} ->
313
        raise error
1✔
314
    end
315
  end
316

317
  @doc false
318
  def encrypt(config, plaintext, label) do
319
    case config[:ciphers][label] do
6✔
320
      nil ->
2✔
321
        {:error, Cloak.MissingCipher.exception(vault: config[:vault], label: label)}
322

323
      {module, opts} ->
324
        module.encrypt(plaintext, opts)
4✔
325
    end
326
  end
327

328
  @doc false
329
  def encrypt!(config, plaintext, label) do
330
    case encrypt(config, plaintext, label) do
3✔
331
      {:ok, ciphertext} ->
332
        ciphertext
2✔
333

334
      {:error, error} ->
335
        raise error
1✔
336
    end
337
  end
338

339
  @doc false
340
  def decrypt(config, ciphertext) do
341
    case find_module_to_decrypt(config, ciphertext) do
7✔
342
      nil ->
2✔
343
        {:error, Cloak.MissingCipher.exception(vault: config[:vault], ciphertext: ciphertext)}
344

345
      {_label, {module, opts}} ->
346
        module.decrypt(ciphertext, opts)
5✔
347
    end
348
  end
349

350
  @doc false
351
  def decrypt!(config, ciphertext) do
352
    case decrypt(config, ciphertext) do
4✔
353
      {:ok, plaintext} ->
354
        plaintext
3✔
355

356
      {:error, error} ->
357
        raise error
1✔
358
    end
359
  end
360

361
  defp find_module_to_decrypt(config, ciphertext) do
362
    Enum.find(config[:ciphers], fn {_label, {module, opts}} ->
7✔
363
      module.can_decrypt?(ciphertext, opts)
11✔
364
    end)
365
  end
366
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc