├── test ├── test_helper.exs ├── cryptex_test.exs └── cryptex │ ├── serializers │ └── elixir_test.exs │ ├── message_verifier_test.exs │ ├── message_encryptor_test.exs │ └── key_generator_test.exs ├── lib ├── cryptex.ex └── cryptex │ ├── serializers │ ├── null.ex │ └── elixir.ex │ ├── serializer.ex │ ├── message_verifier.ex │ ├── key_generator.ex │ └── message_encryptor.ex ├── .gitignore ├── mix.exs ├── config └── config.exs ├── LICENSE └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/cryptex.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex do 2 | end 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /test/cryptex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CryptexTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert 1 + 1 == 2 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/cryptex/serializers/null.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.Serializers.NULL do 2 | def encode(value), do: value 3 | def decode(value), do: value 4 | end 5 | -------------------------------------------------------------------------------- /lib/cryptex/serializer.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.Serializer do 2 | def convert_serializer(serializer) do 3 | case Atom.to_string(serializer) do 4 | "Elixir." <> _ -> serializer 5 | reference -> Module.concat(Cryptex.Serializers, String.upcase(reference)) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/cryptex/serializers/elixir.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.Serializers.ELIXIR do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Encode the given Elixir term into a binary. 6 | """ 7 | def encode(value) do 8 | :erlang.term_to_binary(value) 9 | end 10 | 11 | @doc """ 12 | Decode the given binary into an Elixir term. 13 | """ 14 | def decode(value) do 15 | :erlang.binary_to_term(value) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/cryptex/serializers/elixir_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.Serializers.ElixirTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cryptex.Serializers.ELIXIR 5 | 6 | test "it encodes values" do 7 | term = %{foo: "bar"} 8 | encoded = "837400000001640003666F6F6D00000003626172" 9 | assert encoded == ELIXIR.encode(term) |> Base.encode16 10 | end 11 | 12 | test "it decodes values" do 13 | encoded = "837400000001640003666F6F6D00000003626172" 14 | assert ELIXIR.decode(encoded |> Base.decode16!).foo == "bar" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.Mixfile do 2 | use Mix.Project 3 | 4 | @description """ 5 | An Elixir library for encrypting/decrypting, signing/verifying data. 6 | """ 7 | 8 | def project do 9 | [app: :cryptex, 10 | version: "0.0.1", 11 | elixir: ">= 0.14.0", 12 | description: @description, 13 | package: package] 14 | end 15 | 16 | def application do 17 | [applications: []] 18 | end 19 | 20 | defp package do 21 | [ 22 | files: ["lib", "mix.exs", "README*", "LICENSE"], 23 | contributors: ["Sonny Scroggin"], 24 | licenses: ["MIT"], 25 | links: [ { "GitHub", "https://github.com/scrogson/cryptex" } ] 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies. The Mix.Config module provides functions 3 | # to aid in doing so. 4 | use Mix.Config 5 | 6 | # Note this file is loaded before any dependency and is restricted 7 | # to this project. If another project depends on this project, this 8 | # file won't be loaded nor affect the parent project. 9 | 10 | # Sample configuration: 11 | # 12 | # config :my_dep, 13 | # key: :value, 14 | # limit: 42 15 | 16 | # It is also possible to import configuration files, relative to this 17 | # directory. For example, you can emulate configuration per environment 18 | # by uncommenting the line below and defining dev.exs, test.exs and such. 19 | # Configuration from the imported file will override the ones defined 20 | # here (which is why it is important to import them last). 21 | # 22 | # import_config "#{Mix.env}.exs" 23 | -------------------------------------------------------------------------------- /test/cryptex/message_verifier_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.MessageVerifierTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cryptex.MessageVerifier, as: MV 5 | 6 | test "generates a signed message" do 7 | [content, encoded] = String.split MV.generate("secret", :hello), "--" 8 | assert content |> Base.decode64! |> :erlang.binary_to_term == :hello 9 | assert byte_size(encoded) == 40 10 | end 11 | 12 | test "verifies a signed message" do 13 | signed = MV.generate("secret", :hello) 14 | assert MV.verify("secret", signed) == {:ok, :hello} 15 | end 16 | 17 | test "does not verify a signed message if secret changed" do 18 | signed = MV.generate("secret", :hello) 19 | assert MV.verify("secreto", signed) == :error 20 | end 21 | 22 | test "does not verify a tampered message" do 23 | [_, encoded] = String.split MV.generate("secret", :hello), "--" 24 | content = :bye |> :erlang.term_to_binary |> Base.encode64 25 | assert MV.verify("secret", content <> "--" <> encoded) == :error 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/cryptex/message_encryptor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.MessageEncryptorTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cryptex.KeyGenerator, as: KG 5 | alias Cryptex.MessageEncryptor, as: ME 6 | 7 | @secret_key_base "072d1e0157c008193fe48a670cce031faa4e6844b84326f6d31de759ad1820a928c9fc288c756d847b96c06afcdb8f04f80f38a84382028ebd6a783b59ab90b8" 8 | @encrypted_cookie_salt "encrypted cookie" 9 | @encrypted_signed_cookie_salt "signed encrypted cookie" 10 | 11 | setup do 12 | secret = KG.generate(@secret_key_base, @encrypted_cookie_salt) 13 | sign_secret = KG.generate(@secret_key_base, @encrypted_signed_cookie_salt) 14 | encryptor = ME.new(secret, sign_secret) 15 | {:ok, %{encryptor: encryptor}} 16 | end 17 | 18 | test "it encrypts/decrypts a message", %{encryptor: encryptor} do 19 | data = %{current_user: %{name: "José"}} 20 | encrypted = ME.encrypt_and_sign(encryptor, data) 21 | decrypted = ME.decrypt_and_verify(encryptor, encrypted) 22 | assert "José" == decrypted.current_user.name 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sonny Scroggin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cryptex 2 | ======= 3 | 4 | An [Elixir] library for encrypting/decrypting, signing/verifying data. 5 | 6 | ## Attention! This repo has been archived. 7 | 8 | This package is no longer being maintained and should not be used. The original 9 | code was developed to be included into [Plug]. It was merged into Plug in [this 10 | pull-request] and will continue to be maintained there. 11 | 12 | Using this package could cause your software to become vulnerable to attack. 13 | 14 | [Elixir]: http://elixir-lang.org 15 | [Plug]: https://github.com/elixir-plug/plug 16 | [this pull-request]: https://github.com/elixir-plug/plug/pull/72 17 | 18 | ## License 19 | 20 | The MIT License (MIT) 21 | 22 | Copyright (c) 2014 Sonny Scroggin 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in all 32 | copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | SOFTWARE. 41 | -------------------------------------------------------------------------------- /test/cryptex/key_generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.KeyGeneratorTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Cryptex.KeyGenerator 5 | use Bitwise 6 | 7 | @max_length bsl(1, 32) - 1 8 | 9 | test "returns an :error tuple when the length is too large" do 10 | error = {:error, :derived_key_too_long} 11 | assert generate("secret", "salt", length: @max_length + 1) == error 12 | end 13 | 14 | test "it works" do 15 | key = generate("password", "salt", iterations: 1, length: 20) 16 | assert byte_size(key) == 20 17 | assert to_hex(key) == "0c60c80f961f0e71f3a9b524af6012062fe037a6" 18 | 19 | key = generate("password", "salt", iterations: 2, length: 20) 20 | assert byte_size(key) == 20 21 | assert to_hex(key) == "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957" 22 | 23 | key = generate("password", "salt", iterations: 4096, length: 20) 24 | assert byte_size(key) == 20 25 | assert to_hex(key) == "4b007901b765489abead49d926f721d065a429c1" 26 | 27 | key = generate("passwordPASSWORDpassword", "saltSALTsaltSALTsaltSALTsaltSALTsalt", iterations: 4096, length: 25) 28 | assert byte_size(key) == 25 29 | assert to_hex(key) == "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038" 30 | 31 | key = generate("pass\0word", "sa\0lt", iterations: 4096, length: 16) 32 | assert byte_size(key) == 16 33 | assert to_hex(key) == "56fa6aa75548099dcc37d7f03425e0c3" 34 | 35 | key = generate("password", "salt") 36 | assert byte_size(key) == 32 37 | assert to_hex(key) == "6e88be8bad7eae9d9e10aa061224034fed48d03fcbad968b56006784539d5214" 38 | end 39 | 40 | test ":sha256" do 41 | key = generate("password", "salt", digest: :sha256) 42 | assert byte_size(key) == 32 43 | assert to_hex(key) == "632c2812e46d4604102ba7618e9d6d7d2f8128f6266b4a03264d2a0460b7dcb3" 44 | end 45 | 46 | def to_hex(value), do: Base.encode16(value, case: :lower) 47 | end 48 | -------------------------------------------------------------------------------- /lib/cryptex/message_verifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.MessageVerifier do 2 | @moduledoc """ 3 | `MessageVerifier` makes it easy to generate and verify messages 4 | which are signed to prevent tampering. 5 | 6 | For example, the cookie store uses this verifier to send data 7 | to the client. Although the data can be read by the client, he 8 | cannot tamper it. 9 | """ 10 | 11 | import Cryptex.Serializer 12 | 13 | @doc """ 14 | Decodes and verifies the encoded binary was not tampared with. 15 | """ 16 | def verify(secret, encoded, serializer \\ :elixir) do 17 | case String.split(encoded, "--") do 18 | [content, digest] when content != "" and digest != "" -> 19 | if secure_compare(digest(secret, content), digest) do 20 | { :ok, content |> Base.decode64! |> convert_serializer(serializer).decode} 21 | else 22 | :error 23 | end 24 | _ -> 25 | :error 26 | end 27 | end 28 | 29 | @doc """ 30 | Generates an encoded and signed binary for the given term. 31 | """ 32 | def generate(secret, term, serializer \\ :elixir) do 33 | encoded = term |> convert_serializer(serializer).encode |> Base.encode64 34 | encoded <> "--" <> digest(secret, encoded) 35 | end 36 | 37 | defp digest(secret, data) do 38 | <> = :crypto.hmac(:sha, secret, data) 39 | Integer.to_char_list(mac, 16) |> IO.iodata_to_binary 40 | end 41 | 42 | @doc """ 43 | Compares the two binaries completely, byte by byte, 44 | to avoid timing attacks. 45 | """ 46 | def secure_compare(left, right) do 47 | if byte_size(left) == byte_size(right) do 48 | compare_each(left, right, true) 49 | else 50 | false 51 | end 52 | end 53 | 54 | defp compare_each(<>, <>, acc) do 55 | compare_each(left, right, acc) 56 | end 57 | 58 | defp compare_each(<<_, left :: binary>>, <<_, right :: binary>>, _acc) do 59 | compare_each(left, right, false) 60 | end 61 | 62 | defp compare_each(<<>>, <<>>, acc) do 63 | acc 64 | end 65 | end 66 | 67 | -------------------------------------------------------------------------------- /lib/cryptex/key_generator.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.KeyGenerator do 2 | @moduledoc """ 3 | KeyGenerator is a simple implementation of PBKDF2. 4 | 5 | It can be used to derive a number of keys for various purposes from a given 6 | secret. This lets applications have a single secure secret, but avoid reusing 7 | that key in multiple incompatible contexts. 8 | """ 9 | 10 | use Bitwise 11 | @max_length bsl(1, 32) - 1 12 | 13 | @doc """ 14 | Returns a derived key suitable for use. 15 | 16 | ## Options 17 | 18 | * `:iterations` - defaults to 1000; 19 | * `:length` - a length in octets for the derived key. Defaults to 32; 20 | * `:digest` - an hmac function to use as the pseudo-random function. 21 | Defaults to `:sha`; 22 | """ 23 | def generate(secret, salt, opts \\ []) do 24 | opts = opts 25 | |> Keyword.put_new(:iterations, 1000) 26 | |> Keyword.put_new(:length, 32) 27 | |> Keyword.put_new(:digest, :sha) 28 | |> Enum.into(%{}) 29 | 30 | generate(mac_fun(opts[:digest]), secret, salt, opts, 1, []) 31 | end 32 | 33 | defp generate(_fun, _secret, _salt, %{length: length}, _, _) 34 | when length > @max_length, do: {:error, :derived_key_too_long} 35 | 36 | defp generate(fun, secret, salt, opts, block_index, acc) do 37 | length = opts[:length] 38 | if IO.iodata_length(acc) > length do 39 | key = acc |> Enum.reverse |> IO.iodata_to_binary 40 | <> = key 41 | bin 42 | else 43 | block = generate(fun, secret, salt, opts, block_index, 1, "", "") 44 | generate(fun, secret, salt, opts, block_index + 1, [block, acc]) 45 | end 46 | end 47 | 48 | defp generate(_fun, _secret, _salt, %{iterations: iterations}, _block_index, iteration, _prev, acc) 49 | when iteration > iterations, do: acc 50 | 51 | defp generate(fun, secret, salt, opts, block_index, 1, _prev, _acc) do 52 | initial = fun.(secret, <>) 53 | generate(fun, secret, salt, opts, block_index, 2, initial, initial) 54 | end 55 | 56 | defp generate(fun, secret, salt, opts, block_index, iteration, prev, acc) do 57 | next = fun.(secret, prev) 58 | generate(fun, secret, salt, opts, block_index, iteration + 1, next, :crypto.exor(next, acc)) 59 | end 60 | 61 | defp mac_fun(digest) do 62 | fn key, data -> 63 | :crypto.hmac(digest, key, data) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/cryptex/message_encryptor.ex: -------------------------------------------------------------------------------- 1 | defmodule Cryptex.MessageEncryptor do 2 | @moduledoc ~S""" 3 | `MessageEncryptor` is a simple way to encrypt values which get stored 4 | somewhere you don't trust. 5 | 6 | The cipher text and initialization vector are base64 encoded and 7 | returned to you. 8 | 9 | This can be used in situations similar to the `MessageVerifier`, but where 10 | you don't want users to be able to determine the value of the payload. 11 | 12 | ## Example 13 | 14 | secret_key_base = "072d1e0157c008193fe48a670cce031faa4e..." 15 | encrypted_cookie_salt = "encrypted cookie" 16 | encrypted_signed_cookie_salt = "signed encrypted cookie" 17 | 18 | secret = KeyGenerator.generate(secret_key_base, encrypted_cookie_salt) 19 | sign_secret = KeyGenerator.generate(secret_key_base, encrypted_signed_cookie_salt) 20 | encryptor = MessageEncryptor.new(secret, sign_secret) 21 | 22 | data = %{current_user: %{name: "José"}} 23 | encrypted = MessageEncryptor.encrypt_and_sign(encryptor, data) 24 | decrypted = MessageEncryptor.decrypt_and_verify(encryptor, encrypted) 25 | decrypted.current_user.name # => "José" 26 | """ 27 | 28 | alias Cryptex.MessageVerifier 29 | import Cryptex.Serializer 30 | 31 | def new(secret, sign_secret, opts \\ []) do 32 | opts = opts 33 | |> Keyword.put_new(:cipher, :aes_cbc256) 34 | |> Keyword.put_new(:serializer, :elixir) 35 | 36 | %{ 37 | secret: secret, 38 | sign_secret: sign_secret, 39 | cipher: opts[:cipher], 40 | serializer: convert_serializer(opts[:serializer]) 41 | } 42 | end 43 | 44 | def encrypt_and_sign(encryptor, message) do 45 | iv = :crypto.strong_rand_bytes(16) 46 | 47 | encrypted = message 48 | |> encryptor.serializer.encode 49 | |> pad_message 50 | |> encrypt(encryptor.cipher, encryptor.secret, iv) 51 | 52 | encrypted = "#{Base.encode64(encrypted)}--#{Base.encode64(iv)}" 53 | MessageVerifier.generate(encryptor.sign_secret, encrypted, :null) 54 | end 55 | 56 | def decrypt_and_verify(encryptor, encrypted) do 57 | {:ok, verified} = MessageVerifier.verify(encryptor.sign_secret, encrypted, :null) 58 | [encrypted, iv] = String.split(verified, "--") |> Enum.map(&Base.decode64!/1) 59 | 60 | message = encrypted 61 | |> decrypt(encryptor.cipher, encryptor.secret, iv) 62 | |> unpad_message 63 | |> encryptor.serializer.decode 64 | end 65 | 66 | defp encrypt(message, cipher, secret, iv) do 67 | :crypto.block_encrypt(cipher, secret, iv, message) 68 | end 69 | 70 | defp decrypt(encrypted, cipher, secret, iv) do 71 | :crypto.block_decrypt(cipher, secret, iv, encrypted) 72 | end 73 | 74 | defp pad_message(msg) do 75 | bytes_remaining = rem(byte_size(msg) + 1, 16) 76 | padding_size = if bytes_remaining == 0, do: 0, else: 16 - bytes_remaining 77 | <> <> msg <> :crypto.strong_rand_bytes(padding_size) 78 | end 79 | 80 | defp unpad_message(msg) do 81 | <> = msg 82 | msg_size = byte_size(rest) - padding_size 83 | <> = rest 84 | msg 85 | end 86 | end 87 | --------------------------------------------------------------------------------