Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dvv committed Jan 15, 2013
0 parents commit 8c6269d
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ebin
deps
.eunit
.ct
logs
test/*.beam
erl_crash.dump
18 changes: 18 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Copyright (c) 2013 Vladimir Dronnikov <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
all: deps compile check test

deps:
rebar get-deps

compile:
rebar compile

run: compile
sh start.sh

clean:
rebar clean
rm -fr ebin .ct test/*.beam

check:
rebar eunit skip_deps=true

test:
#rebar ct
mkdir -p .ct
ct_run -dir test -logdir .ct -pa ebin

dist: deps compile
echo TODO

.PHONY: all deps compile check test run clean dist
.SILENT:

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Termit
==============

Library for serializing Erlang terms to signed encrypted binaries and reliably deserializing them back.

Usage
--------------

A typical use case is to provide means to keep secrets put in public domain, e.g. secure cookies.

```erlang
Term = {this, is, an, [erlang, <<"term">>]}.
Cookie = termit:encode_base64(Term, <<"cekpet">>).

% time-to-live is 1000 seconds =:= secret valid no more than 1000 seconds
{ok, Term} = termit:decode_base64(Cookie, <<"cekpet">>, 1000).
% time-to-live is 0 seconds =:= expired
{error, expired} = termit:decode_base64(Cookie, <<"cekpet">>, 0).

% check whether secret was not forged
{error, forged} = termit:decode_base64(<<Cookie/binary, "1">>, <<"cekpet">>, 1000).
{error, forged} = termit:decode_base64(Cookie, <<"secret">>, 1000).
{error, forged} = termit:decode_base64(undefined, <<"cekpet">>, 1000).
```

[License](termit/blob/master/LICENSE.txt)
-------

Copyright (c) 2013 Vladimir Dronnikov <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 changes: 11 additions & 0 deletions rebar.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{lib_dirs, ["deps"]}.

{erl_opts, [
debug_info,
warn_format,
warn_export_vars,
warn_obsolete_guard,
warn_bif_clash
]}.

{cover_enabled, true}.
10 changes: 10 additions & 0 deletions src/termit.app.src
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{application, termit, [
{description, "Securely serialize/deserialize Erlang terms"},
{vsn, "0.0.1"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{env, []}
]}.
162 changes: 162 additions & 0 deletions src/termit.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
%%
%% @doc Serialize an Erlang term to signed encrypted binary and
%% deserialize it back ensuring it's not been forged.
%%

-module(termit).
-author('Vladimir Dronnikov <[email protected]>').

-export([
encode/2, decode/3,
encode_base64/2, decode_base64/3
]).

%%
%% -----------------------------------------------------------------------------
%% @doc Serialize Term, encrypt and sign the result with Secret.
%% Return binary().
%% -----------------------------------------------------------------------------
%%

-spec encode(Term :: any(), Secret :: binary()) -> Cipher :: binary().

encode(Term, Secret) ->
Bin = term_to_binary(Term),
Enc = encrypt(Bin, Secret),
{MegaSecs, Secs, _} = erlang:now(),
Time = list_to_binary(integer_to_list(MegaSecs * 1000000 + Secs)),
Sig = sign(<<Time/binary, Enc/binary>>, Secret),
<<Sig/binary, Time/binary, Enc/binary>>.

%%
%% -----------------------------------------------------------------------------
%% @doc Given a result of encode/2, i.e. a signed encrypted binary,
%% check the signature, uncrypt and deserialize into original term.
%% Check it timestamp encoded into the data is not older than Ttl.
%% Return {ok, Term} or {error, Reason}.
%% -----------------------------------------------------------------------------
%%

-spec decode(
Cipher :: binary(),
Secret :: binary(),
Ttl :: non_neg_integer()
) -> {ok, Term :: any()} | {error, Reason :: atom()}.

%% @todo how do we know time is 10 octets?

decode(<<Sig:32/binary, Time:10/binary, Enc/binary>>, Secret, Ttl) ->
case sign(<<Time/binary, Enc/binary>>, Secret) of
% signature ok?
Sig ->
Bin = uncrypt(Enc, Secret),
% deserialize
try binary_to_term(Bin, [safe]) of
Term ->
% not yet expired?
{MegaSecs, Secs, _} = erlang:now(),
Now = MegaSecs * 1000000 + Secs,
Expires = list_to_integer(binary_to_list(Time)) + Ttl,
case Expires > Now of
true ->
{ok, Term};
false ->
{error, expired}
end
catch _:_ ->
{error, badarg}
end;
_ ->
{error, forged}
end;

%% N.B. unmatched binaries are forged
decode(Bin, _, _) when is_binary(Bin) ->
{error, forged}.

%%
%% -----------------------------------------------------------------------------
%% @doc Get 32-byte SHA1 sum of Data salted with Secret.
%% -----------------------------------------------------------------------------
%%

-spec sign(binary(), binary()) -> binary().

sign(Data, Secret) ->
crypto:sha256([Data, Secret]).

%%
%% -----------------------------------------------------------------------------
%% @doc Encrypt Bin using Secret.
%% -----------------------------------------------------------------------------
%%

-spec encrypt(binary(), binary()) -> binary().

encrypt(Bin, Secret) ->
<<Key:16/binary, IV:16/binary>> = crypto:sha256(Secret),
crypto:aes_cfb_128_encrypt(Key, IV, Bin).

%%
%% -----------------------------------------------------------------------------
%% @doc Uncrypt Bin using Secret.
%% -----------------------------------------------------------------------------
%%

-spec uncrypt(binary(), binary()) -> binary().

uncrypt(Bin, Secret) ->
<<Key:16/binary, IV:16/binary>> = crypto:sha256(Secret),
crypto:aes_cfb_128_decrypt(Key, IV, Bin).

%%
%% -----------------------------------------------------------------------------
%% Conversion helpers
%% -----------------------------------------------------------------------------
%%

encode_base64(Term, Secret) ->
base64:encode(encode(Term, Secret)).

decode_base64(undefined, _, _) ->
{error, forged};

decode_base64(Bin, Secret, Ttl) when is_binary(Bin) ->
decode(base64:decode(Bin), Secret, Ttl).

%%
%% -----------------------------------------------------------------------------
%% Some unit tests
%% -----------------------------------------------------------------------------
%%

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

encrypt_test() ->
Secret = <<"Make It Elegant">>,
Bin = <<"Transire Benefaciendo">>,
?assertEqual(Bin, uncrypt(encrypt(Bin, Secret), Secret)),
?assert(Bin =/= uncrypt(encrypt(Bin, Secret), <<Secret/binary, "1">>)),
?assert(Bin =/= uncrypt(encrypt(Bin, <<Secret/binary, "1">>), Secret)).

smoke_test() ->
Term = {a, b, c, [d, "e", <<"foo">>]},
Secret = <<"TopSecRet">>,
Enc = encode(Term, Secret),
% decode encoded term with valid time to live
?assertEqual({ok, Term}, decode(Enc, Secret, 1)),
% expired data
?assertEqual({error, expired}, decode(encode(Term, Secret), Secret, 0)),
% forged data
?assertEqual({error, forged}, decode(<<"1">>, Secret, 1)),
?assertEqual({error, forged}, decode(<<Enc/binary, "1">>, Secret, 1)).

encode_test(_Config) ->
Term = {a, b, c, [d, "e", <<"foo">>]},
Secret = <<"TopSecRet">>,
undefined = decode_base64(undefined, a, b),
{ok, Term} = decode_base64(encode_base64(Term, Secret), Secret, 1),
{error, expired} = decode_base64(encode_base64(Term, Secret), Secret, 0).

-endif.
22 changes: 22 additions & 0 deletions test/termit_SUITE.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-module(termit_SUITE).
-author('Vladimir Dronnikov <[email protected]>').

%% interface
-export([all/0]).

%% tests
-export([encode_test/1]).

-include_lib("common_test/include/ct.hrl").

all() ->
[encode_test].

encode_test(_Config) ->
Term = {a, b, c, [d, "e", <<"foo">>]},
Secret = <<"TopSecRet">>,
{error, forged} = termit:decode_base64(undefined, a, b),
{ok, Term} = termit:decode_base64(
termit:encode_base64(Term, Secret), Secret, 1),
{error, expired} = termit:decode_base64(
termit:encode_base64(Term, Secret), Secret, 0).

0 comments on commit 8c6269d

Please sign in to comment.