Skip to content
/ erebus Public

Erebus is an implementation of the envelope encryption paradigm.

License

Notifications You must be signed in to change notification settings

venndr/erebus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Erebus

Elixir CI

Erebus is an implementation of the envelope encryption paradigm. It uses a separate key (called DEK, short for data encryption key) for each encrypted struct. The key is regenerated on each save, making key encryption barely necessary. During each encryption, the DEK is encrypted using the KEK (key encryption key).

The DEK is a symmetric key (Erebus uses AES-256 with Galois mode (AES-GCM) with AEAD), which guarantees both the security and the integrity of the data. The KEK is an asymmetric key – Erebus uses the public key for encryption (for performance reasons when using external key storage) and the private key for decryption. The specific implementation depends on the backend.

Currently, there are three supported backend implementations:

  • Erebus.KMS.Google - Google KMS key storage. Means that your private key never leaves Google infrastructure, which is the most secure option
  • Erebus.KMS.Local - private/public key pair stored on your hard drive. Please note that it makes them prone to leakage
  • Erebus.KMS.Dummy - base64 as encryption for DEK. Never use it in production

Please note that you need to provide config for the operations and explicitly invoke them, providing the config on every call.

Installation

Erebus can be installed by adding erebus to your list of dependencies in mix.exs:

def deps do
  [
    {:erebus, "~> 0.2.0-rc.3"}
  ]
end

Usage

To use Erebus you need to wrap it to provide your backend configuration. Create a module like the example below in your app:

defmodule MyApp.Erebus do
  def encrypt(struct, handle, version) do
    opts = Application.get_env(:my_app, :erebus)

    Erebus.encrypt(struct, handle, version, opts)
  end

  def decrypt(struct, fields) do
    opts = Application.get_env(:my_app, :erebus)

    Erebus.decrypt(struct, fields, opts)
  end
end

In an encrypted struct, Erebus requires several fields for each encrypted property. Follow the example below to define your structs:

defmodule SecureModel
  @type t :: %__MODULE__{
      # `first` is an encrypted field
      first: String.t(),
      # `second` is another encrypted field
      second: String.t(),

      # this field contains the ciphertext for the `first` field
      first_encrypted: binary(),
      # this field contains the ciphertext for the `second` field
      second_encrypted: binary(),

      # this field contains a hashed version of the unencrypted `first` field
      first_hash: String.t(),
      # this field contains a hashed version of the unencrypted `second` field
      second_hash: String.t(),

      # this field contains the DEK (data encryption key)
      dek: map()
    }
end

In this example first and second are the names of the encryptable fields. The unsuffixed fields are virtual, meaning they are only used for encrypting (before write), or are populated after decrypting the equivalent *_encrypted fields – only suffixed fields are written to the database. The dek field contains the DEK.

The *_hash suffixed fields are hashed (using SHA512) versions of the plain text field content. They can be used for finding exact matches without having to decrypt the content.

When using Ecto, fields are defined using the hashed_encrypted_field(:field_name) and data_encryption_key() macros, which create all the necessary auxiliary fields for you:

use Erebus.Schema

embedded_schema "table" do
  hashed_encrypted_field(:first)
  hashed_encrypted_field(:second)
  data_encryption_key()
end

Additionally you must implement the Erebus.Encryption protocol to mark the fields which should be encrypted:

defimpl Erebus.Encryption do
  def encrypted_fields(_), do: [:first, :second]
end

Usage with local KMS adapter

  1. Set the following values in your application config:
    config :my_app, :erebus,
      kms_backend: Erebus.KMS.Local,
      keys_base_path: "path_to_directory_containing_a_key_pair",
      private_key_password: "1234"
  2. Generate asymmetric key pairs (named public.pem and private.pem) in the folder at keys_base_path.

Usage with Google KMS adapter

  1. Include Goth in your application:
    {:goth, "~> 1.3.0-rc.2"}
  2. Start Goth in your application.ex:
    credentials =
      "GCP_KMS_CREDENTIALS_PATH"
      |> System.fetch_env!()
      |> File.read!()
      |> Jason.decode!()
    
    scopes = ["https://www.googleapis.com/auth/cloudkms"]
    
    source = {:service_account, credentials, scopes: scopes}
    
    children = [
      {Goth, name: MyApp.Goth, source: source}
    ]
  3. Pass the app's name in the google_goth option to Erebus:
    config :my_app, :erebus,
      kms_backend: Erebus.KMS.Google,
      google_project: "someproject",
      google_region: "someregion",
      google_keyring: "some_keyring",
      google_goth: MyApp.Goth

Please note that if you're using Google KMS, your key must have access to the following roles:

Ecto usage full example

defmodule EncryptedStuff do
    use Ecto.Schema
    import Ecto.Changeset
    use Erebus.Schema

    embedded_schema do
      hashed_encrypted_field(:first)
      hashed_encrypted_field(:second)
      data_encryption_key()
      field(:other, :string)
    end

    def changeset(stuff, attrs) do
      changes =
        stuff
        |> cast(attrs, [
          :first,
          :second
        ])

      encrypted_data =
        changes
        |> MyApp.Erebus.encrypt("handle", 1)

      Ecto.Changeset.change(changes, encrypted_data)
    end

    defimpl Erebus.Encryption do
      def encrypted_fields(_), do: [:first, :second]
    end
  end

If you don't need multiple encryption keys, provide at hard-coded in MyApp.Erebus.

Currently Erebus only supports encoding and decoding data using Ecto changeset.

Licence

ISC License

Copyright (c) 2024 Music Glue

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

A copy of the licence text is also included in the file LICENSE