Permalink
Browse files

Demonstrate key migration

  • Loading branch information...
danielberkompas committed Jul 9, 2015
1 parent 9e17198 commit 629f4f4de6987a0d9106cbe7280201595cbb79a5
View
@@ -33,4 +33,8 @@ config :encryption, Encryption.Repo,
size: 10 # The amount of database connections in the pool
config :encryption, Encryption.AES,
- key: :base64.decode("vA/K/7K6Z3obnTxlPx6fDuy/tiPj4FS7dDtUpfvRbG4=")
+ keys: %{
+ <<1>> => :base64.decode("vA/K/7K6Z3obnTxlPx6fDuy/tiPj4FS7dDtUpfvRbG4="),
+ <<2>> => :base64.decode("Vnx9YGmqSHx9nTc3lVQxRwYS0A8Fks7fGzuMLZISl30=")
+ },
+ default: <<1>>
View
@@ -17,4 +17,8 @@ config :encryption, Encryption.Repo,
size: 1
config :encryption, Encryption.AES,
- key: :base64.decode("vA/K/7K6Z3obnTxlPx6fDuy/tiPj4FS7dDtUpfvRbG4=")
+ keys: %{
+ <<1>> => :base64.decode("vA/K/7K6Z3obnTxlPx6fDuy/tiPj4FS7dDtUpfvRbG4="),
+ <<2>> => :base64.decode("Vnx9YGmqSHx9nTc3lVQxRwYS0A8Fks7fGzuMLZISl30=")
+ },
+ default: <<1>>
View
@@ -4,6 +4,12 @@ defmodule Encryption.AES do
See `encrypt/1` and `decrypt/1` for more details.
"""
+ @config Application.get_env(:encryption, Encryption.AES)
+ @keys @config[:keys]
+ @default @config[:default]
+
+ @type key_id :: binary
+
@doc """
Encrypt a value. Uses a random IV for each call, and prepends the IV to the
ciphertext. This means that `encrypt/1` will never return the same ciphertext
@@ -22,23 +28,27 @@ defmodule Encryption.AES do
iex> ciphertext = Encryption.AES.encrypt(123)
...> is_binary(ciphertext)
true
+
+ iex> ciphertext = Encryption.AES.encrypt(123, <<2>>)
+ ...> is_binary(ciphertext)
+ true
"""
- @spec encrypt(any) :: String.t
- def encrypt(plaintext) do
+ @spec encrypt(any, key_id) :: String.t
+ def encrypt(plaintext, key_id \\ @default) do
iv = :crypto.strong_rand_bytes(16)
- state = :crypto.stream_init(:aes_ctr, key, iv)
-
+ state = :crypto.stream_init(:aes_ctr, @keys[key_id], iv)
{_state, ciphertext} = :crypto.stream_encrypt(state, to_string(plaintext))
- iv <> ciphertext
+ key_id <> iv <> ciphertext
end
@doc """
Decrypt a binary.
## Parameters
- - `ciphertext`: a binary to decrypt, assuming that the first 16 bytes of the
- binary are the IV to use for decryption.
+ - `ciphertext`: a binary to decrypt, assuming that the first byte is the ID
+ of the key that was used to encrypt, the next 16 bytes are the IV, and the
+ remainder is the ciphertext.
## Example
@@ -47,14 +57,13 @@ defmodule Encryption.AES do
"""
@spec decrypt(String.t) :: String.t
def decrypt(ciphertext) do
- <<iv::binary-16, ciphertext::binary>> = ciphertext
- state = :crypto.stream_init(:aes_ctr, key, iv)
-
+ <<key_id::binary-1, iv::binary-16, ciphertext::binary>> = ciphertext
+ state = :crypto.stream_init(:aes_ctr, @keys[key_id], iv)
{_state, plaintext} = :crypto.stream_decrypt(state, ciphertext)
plaintext
end
- defp key do
- Application.get_env(:encryption, Encryption.AES)[:key]
+ def key_id do
+ @default
end
end
@@ -0,0 +1,38 @@
+defmodule Mix.Tasks.Encryption.Migrate do
+ use Mix.Task
+
+ import Ecto.Query
+ import Logger, only: [info: 1]
+
+ alias Encryption.Repo
+
+ @key_id Encryption.AES.key_id
+
+ def run(args) do
+ Mix.Task.run "app.start", args
+ migrate Encryption.User
+ end
+
+ defp migrate(model) do
+ info "=== Migrating #{model} Model ==="
+ ids = ids_for(model)
+ info "#{length(ids)} records found needing migration"
+
+ for id <- ids do
+ Repo.get(model, id) |> migrate_record
+ end
+
+ info "=== Migration Complete ==="
+ end
+
+ defp ids_for(model) do
+ query = from m in model, where: m.encryption_key_id != ^@key_id,
+ select: m.id
+ Repo.all(query)
+ end
+
+ # Do nothing if the record has been migrated by app usage since
+ # we queried for ids.
+ defp migrate_record(%{encryption_key_id: @key_id}), do: nil
+ defp migrate_record(record), do: Repo.update!(record)
+end
@@ -0,0 +1,11 @@
+defmodule Encryption.Repo.Migrations.AddEncryptionKeyIdToUser do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add :encryption_key_id, :binary
+ end
+
+ create index(:users, [:encryption_key_id])
+ end
+end
View
@@ -8,19 +8,30 @@ defmodule Encryption.AESTest do
assert AES.encrypt("hello") != "hello"
end
- test ".encrypt includes the random IV in the value" do
- <<iv::binary-16, ciphertext::binary>> = AES.encrypt("hello")
+ test ".encrypt includes the key id and random IV in the value" do
+ <<key_id::binary-1, iv::binary-16, ciphertext::binary>> = AES.encrypt("hello")
- assert String.length(iv) != 0
- assert String.length(ciphertext) != 0
+ assert String.length(key_id) == 1
+ assert String.length(iv) == 16
+ assert String.length(ciphertext) == 5
end
test ".encrypt does not produce the same ciphertext twice" do
assert AES.encrypt("hello") != AES.encrypt("hello")
end
- test "can decrypt a value" do
+ test ".encrypt can encrypt with a custom key" do
+ assert AES.encrypt("hello", <<2>>) != "hello"
+ end
+
+ test ".decrypt can decrypt a value" do
plaintext = "hello" |> AES.encrypt |> AES.decrypt
assert plaintext == "hello"
end
+
+ test ".decrypt can decrypt values encrypted with either key" do
+ cipher1 = AES.encrypt("hello", <<1>>)
+ cipher2 = AES.encrypt("hello", <<2>>)
+ assert AES.decrypt(cipher1) == AES.decrypt(cipher2)
+ end
end
@@ -28,6 +28,7 @@ defmodule Encryption.UserTest do
assert user.name == @valid_attrs.name
assert user.email == @valid_attrs.email
assert user.email_hash == Encryption.HashField.hash(@valid_attrs.email)
+ assert user.encryption_key_id == <<1>>
end
test "inserting a user updates the :email_hash field" do
View
@@ -5,13 +5,13 @@ defmodule Encryption.User do
field :name, Encryption.EncryptedField
field :email, Encryption.EncryptedField
field :email_hash, Encryption.HashField
+ field :encryption_key_id, :binary
timestamps
end
- # Ensure that hashed fields never get out of date
- before_insert :set_hashed_fields
- before_update :set_hashed_fields
+ before_insert :set_defaults
+ before_update :set_defaults
@required_fields ~w(name email)
@optional_fields ~w()
@@ -25,12 +25,13 @@ defmodule Encryption.User do
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
- |> set_hashed_fields
+ |> set_defaults
|> validate_unique(:email_hash, on: Encryption.Repo)
end
- defp set_hashed_fields(changeset) do
+ defp set_defaults(changeset) do
changeset
|> put_change(:email_hash, get_field(changeset, :email))
+ |> put_change(:encryption_key_id, Encryption.AES.key_id)
end
end

0 comments on commit 629f4f4

Please sign in to comment.