Skip to content

Commit

Permalink
WIP road to 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rubencaro committed Mar 6, 2016
1 parent 8792720 commit 24a5171
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 95 deletions.
3 changes: 3 additions & 0 deletions config/config.exs
@@ -0,0 +1,3 @@
use Mix.Config

import_config "#{Mix.env}.exs"
5 changes: 5 additions & 0 deletions config/dev.exs
@@ -0,0 +1,5 @@
use Mix.Config

config :cipher, keyphrase: "testiekeyphraseforcipher",
ivphrase: "testieivphraseforcipher",
magic_token: "magictoken"
1 change: 1 addition & 0 deletions config/prod.exs
@@ -0,0 +1 @@
use Mix.Config
5 changes: 5 additions & 0 deletions config/test.exs
@@ -0,0 +1,5 @@
use Mix.Config

config :cipher, keyphrase: "testiekeyphraseforcipher",
ivphrase: "testieivphraseforcipher",
magic_token: "magictoken"
173 changes: 107 additions & 66 deletions lib/cipher.ex
@@ -1,108 +1,100 @@
require Cipher.Helpers, as: H # the cool way

defmodule Cipher do

@moduledoc """
Helpers to encrypt and decrypt data.
"""
# handy to have them around
@k H.env(:keyphrase) |> Cipher.Digest.generate_key
@i H.env(:ivphrase) |> Cipher.Digest.generate_iv

@doc """
Returns encrypted string containing given `data` string, using given `key`
and `iv`.
Suitable `key` and `iv` can be generated with `generate_key/1`
and `generate_iv/1`.
Returns encrypted string containing given `data` string
"""
def encrypt(data, key, iv) do
encrypted = :crypto.block_encrypt :aes_cbc128, key, iv, pad(data)
def encrypt(data) do
encrypted = :crypto.block_encrypt :aes_cbc128, @k, @i, pad(data)
encrypted |> Base.encode64 |> URI.encode_www_form
end

@doc """
Returns decrypted string contained in given `crypted` string, using given
`key` and `iv`.
Suitable `key` and `iv` can be generated with `generate_key/1`
and `generate_iv/1`.
Returns decrypted string contained in given `crypted` string
"""
def decrypt(crypted, key, iv) do
def decrypt(crypted) do
{:ok, decoded} = crypted |> URI.decode_www_form |> Base.decode64
:crypto.block_decrypt(:aes_cbc128, key, iv, decoded) |> depad
end

@doc "Generates a suitable key for encryption based on given `phrase`"
def generate_key(phrase) do
:crypto.hash(:sha, phrase) |> hexdigest |> String.slice(0,16)
end

@doc "Generates a suitable iv for encryption based on given `phrase`"
def generate_iv(phrase), do: phrase |> String.slice(0,16)

@doc "Gets an usable string from a binary crypto hash"
def hexdigest(binary) do
:lists.flatten(for b <- :erlang.binary_to_list(binary),
do: :io_lib.format("~2.16.0B", [b]))
|> :string.to_lower
|> List.to_string
:crypto.block_decrypt(:aes_cbc128, @k, @i, decoded) |> depad
end

@doc """
Pad given string until its length is divisible by 16.
It uses PKCS#7 padding.
Returns the JSON parsed data of the given crypted string, with labeled tuples
"""
def pad(str, block_size \\ 16) do
len = byte_size(str)
utfs = len - String.length(str) # UTF chars are 2byte, ljust counts only 1
pad_len = block_size - rem(len, block_size) - utfs
String.ljust(str, len + pad_len, pad_len) # PKCS#7 padding
def parse(crypted) do
try do
{:ok, crypted |> decrypt |> Poison.decode!}
rescue
reason -> {:error, reason}
end
end

@doc "Remove PKCS#7 padding from given string."
def depad(str) do
<<last>> = String.last str
String.rstrip str, last
end
@doc """
Returns the JSON converted and encrypted version of given data
"""
def cipher(data), do: data |> Poison.encode! |> encrypt

@doc """
Gets signature for given `base` and appends as a param to `url`.
Returns `url` with appended param.
"""
def sign(url, base, key, iv) do
def sign(url, base) do
nexus = if String.contains?(url, "?"), do: "&", else: "?"
signature = :crypto.hash(:md5, base) |> hexdigest
signature = :crypto.hash(:md5, base) |> Cipher.Digest.hexdigest
{_, _, micros} = :os.timestamp
pepper = micros |> Integer.to_string |> String.rjust(8) # 8 characters long
crypted = signature <> pepper |> encrypt(key, iv)
crypted = signature <> pepper |> encrypt
url <> nexus <> "signature=" <> crypted
end

@doc """
An URL is signed by getting a hash from it, ciphering that hash,
and appending it as the last query parameter.
"""
def sign_url(url, key, iv), do: sign(url, url, key, iv)
def sign_url(url), do: sign(url, url)

@doc """
Pops the signature param, which must be the last one.
Returns the remaining url and the popped signature.
Decrypts `ciphered`, and compare with an MD5 hash got from base.
Returns false if decryption failed, or if comparison failed.
Whatever parsed otherwise.
"""
def pop_signature(url) do
case String.split(url, "signature=") do
[dirty_url, popped] ->
clean_url = String.slice dirty_url, 0..-2 # remove nexus
{clean_url, popped}
_ -> {url, ""}
def validate_signature(ciphered, base, rest) do
case parse(ciphered) do
{:ok, parsed} -> validate_parsed_signature(parsed, base, rest)
any -> any
end
end

@doc """
Decrypts `ciphered`, and compare with an MD5 hash got from base.
Returns false if decryption failed, or if comparison failed. True otherwise.
"""
def validate_signature(ciphered, base, key, iv) do
try do
plain = decrypt(ciphered, key, iv)
signature = :crypto.hash(:md5, base) |> hexdigest
signature == String.slice(plain, 0..-9) # removing pepper from parsed
rescue
_ -> false
defp validate_parsed_signature(parsed, base, rest) do
ignored = parsed["data"] |> Map.get("ignore", [])
case validate_ignored(ignored, rest) do
:ok -> validate_base(parsed, base)
:error -> false
end
end

defp validate_ignored(_, []), do: :ok
defp validate_ignored(ignored, [r | rest]) do
case r in ignored do
true -> validate_ignored(ignored, rest)
false -> :error
end
end

defp validate_base(parsed, base) do
signature = :crypto.hash(:md5, base)
|> Cipher.Digest.hexdigest
read_signature = String.slice(parsed["data"]["md5"], 0..-9) # removing pepper from parsed
case signature == read_signature do
true -> parsed["data"]
false -> false
end
end

Expand All @@ -111,9 +103,58 @@ defmodule Cipher do
get an MD5 hash of the remains,
decrypt popped value, and compare with the MD5 hash
"""
def validate_signed_url(url, key, iv) do
{clean_url, popped} = pop_signature(url)
validate_signature(popped, clean_url, key, iv)
def validate_signed_url(url) do
{clean_url, popped, rest} = pop_signature(url)
validate_magic_token(popped, clean_url, rest)
end

@doc """
Pop the last parameter from the URL, decrypt it,
get an MD5 of the body, and compare with the decrypted value
"""
def validate_signed_body(url, body) do
{_, popped, rest} = pop_signature(url)
validate_magic_token(popped, body, rest)
end

defp validate_magic_token(popped, base, rest) do
case popped == H.env(:magic_token) do
true -> true
false -> validate_signature(popped, base, rest)
end
end

# Pad given string until its length is divisible by 16.
# It uses PKCS#7 padding.
#
defp pad(str, block_size \\ 16) do
len = byte_size(str)
utfs = len - String.length(str) # UTF chars are 2byte, ljust counts only 1
pad_len = block_size - rem(len, block_size) - utfs
String.ljust(str, len + pad_len, pad_len) # PKCS#7 padding
end

# Remove PKCS#7 padding from given string.
defp depad(str) do
<<last>> = String.last str
String.rstrip str, last
end

# Pops the signature param, which must be the last one.
# Returns the remaining url and the popped signature.
#
defp pop_signature(url) do
case String.split(url, "signature=") do
[dirty_url, popped] -> pop_signature(dirty_url, popped)
_ -> {url, "", ""}
end
end
defp pop_signature(dirty_url, popped) do
clean_url = String.slice(dirty_url, 0..-2)
case String.split(popped, "&") do
[signature, rest] -> {clean_url, signature, rest}
_ -> {clean_url, popped, ""}
end
end

end
20 changes: 20 additions & 0 deletions lib/cipher/digest.ex
@@ -0,0 +1,20 @@
defmodule Cipher.Digest do

@moduledoc """
Some digesting helpers
"""

@doc "Generates a suitable key for encryption based on given `phrase`"
def generate_key(phrase), do: :crypto.hash(:sha, phrase) |> hexdigest |> String.slice(0,16)

@doc "Generates a suitable iv for encryption based on given `phrase`"
def generate_iv(phrase), do: phrase |> String.slice(0,16)

@doc "Gets an usable string from a binary crypto hash"
def hexdigest(binary) do
:lists.flatten(for b <- :erlang.binary_to_list(binary), do: :io_lib.format("~2.16.0B", [b]))
|> :string.to_lower
|> List.to_string
end

end
47 changes: 47 additions & 0 deletions lib/cipher/helpers.ex
@@ -0,0 +1,47 @@
defmodule Cipher.Helpers do

@moduledoc """
require Cipher.Helpers, as: H # the cool way
"""
@doc """
Convenience to get environment bits. Avoid all that repetitive
`Application.get_env( :myapp, :blah, :blah)` noise.
"""
def env(key, default \\ nil), do: env(Mix.Project.get!.project[:app], key, default)
def env(app, key, default), do: Application.get_env(app, key, default)

@doc """
Spit to output any passed variable, with location information.
"""
defmacro spit(obj \\ "", inspect_opts \\ []) do
quote do
%{file: file, line: line} = __ENV__
name = Process.info(self)[:registered_name]
chain = [ :bright, :red, "\n\n#{file}:#{line}",
:normal, "\n #{inspect self}", :green," #{name}"]

msg = inspect(unquote(obj),unquote(inspect_opts))
if String.length(msg) > 2, do: chain = chain ++ [:red, "\n\n#{msg}"]

# chain = chain ++ [:yellow, "\n\n#{inspect Process.info(self)}"]

(chain ++ ["\n\n", :reset]) |> IO.ANSI.format(true) |> IO.puts

unquote(obj)
end
end

@doc """
Print to stdout a _TODO_ message, with location information.
"""
defmacro todo(msg \\ "") do
quote do
%{file: file, line: line} = __ENV__
[ :yellow, "\nTODO: #{file}:#{line} #{unquote(msg)}\n", :reset]
|> IO.ANSI.format(true)
|> IO.puts
:todo
end
end

end
2 changes: 1 addition & 1 deletion mix.exs
Expand Up @@ -24,7 +24,7 @@ defmodule Cipher.Mixfile do
end

defp deps do
[{:poison, "~> 2.0", only: :test}]
[{:poison, "~> 2.0"}]
end

end

0 comments on commit 24a5171

Please sign in to comment.