You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2020/04/09 16:17:40 UTC

[couchdb] 01/03: Add aegis

This is an automated email from the ASF dual-hosted git repository.

davisp pushed a commit to branch davisp-aegis
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit d54b1675886aacac43cbb19627e8e8dd3149c348
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Thu Apr 9 10:45:05 2020 -0500

    Add aegis
---
 rebar.config.script                 |   1 +
 rel/reltool.config                  |   1 +
 src/aegis/src/aegis.app.src         |  29 ++++++
 src/aegis/src/aegis.erl             | 186 ++++++++++++++++++++++++++++++++++++
 src/aegis/test/aegis_basic_test.erl |  77 +++++++++++++++
 5 files changed, 294 insertions(+)

diff --git a/rebar.config.script b/rebar.config.script
index 6f9f65c..118a99e 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -114,6 +114,7 @@ os:putenv("COUCHDB_APPS_CONFIG_DIR", filename:join([COUCHDB_ROOT, "rel/apps"])).
 SubDirs = [
     %% must be compiled first as it has a custom behavior
     "src/couch_epi",
+    "src/aegis",
     "src/couch_log",
     "src/chttpd",
     "src/couch",
diff --git a/rel/reltool.config b/rel/reltool.config
index 9fbf285..1e64a80 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -90,6 +90,7 @@
     {app, xmerl, [{incl_cond, include}]},
 
     %% couchdb
+    {app, aegis, [{incl_cond, include}]},
     {app, b64url, [{incl_cond, include}]},
     {app, bear, [{incl_cond, include}]},
     {app, chttpd, [{incl_cond, include}]},
diff --git a/src/aegis/src/aegis.app.src b/src/aegis/src/aegis.app.src
new file mode 100644
index 0000000..eb3018f
--- /dev/null
+++ b/src/aegis/src/aegis.app.src
@@ -0,0 +1,29 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+{application, aegis,
+ [{description, "If it's good enough for Zeus, it's good enough for CouchDB"},
+  {vsn, git},
+  {registered, []},
+  {applications,
+   [kernel,
+    stdlib,
+    crypto,
+    erlfdb
+   ]},
+  {env,[]},
+  {modules, []},
+
+  {maintainers, []},
+  {licenses, []},
+  {links, []}
+ ]}.
diff --git a/src/aegis/src/aegis.erl b/src/aegis/src/aegis.erl
new file mode 100644
index 0000000..5937b4c
--- /dev/null
+++ b/src/aegis/src/aegis.erl
@@ -0,0 +1,186 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(aegis).
+
+-define(IS_AEGIS_FUTURE, {aegis_future, _}).
+%% encapsulation violation :/
+-define(IS_FUTURE, {erlfdb_future, _, _}).
+
+%% Assume old crypto api
+-define(hmac(Key, PlainText), crypto:hmac(sha256, Key, PlainText)).
+-define(aes_gcm_encrypt(Key, IV, AAD, Data),
+        crypto:block_encrypt(aes_gcm, Key, IV, {AAD, Data, 16})).
+-define(aes_gcm_decrypt(Key, IV, AAD, CipherText, CipherTag),
+        crypto:block_decrypt(aes_gcm, Key, IV, {AAD, CipherText, CipherTag})).
+
+%% Replace macros if new crypto api is available
+-ifdef(OTP_RELEASE).
+-if(?OTP_RELEASE >= 22).
+-undef(hmac).
+-define(hmac(Key, PlainText), crypto:mac(hmac, sha256, Key, PlainText)).
+-undef(aes_gcm_encrypt).
+-define(aes_gcm_encrypt(Key, IV, AAD, Data),
+        crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, Data, AAD, 16, true)).
+-undef(aes_gcm_decrypt).
+-define(aes_gcm_decrypt(Key, IV, AAD, CipherText, CipherTag),
+        crypto:crypto_one_time_aead(aes_256_gcm, Key, IV, CipherText,
+        AAD, CipherTag, false)).
+-endif.
+-endif.
+
+-export([
+    fold_range/6,
+    fold_range/7,
+    fold_range_future/5,
+    fold_range_wait/4,
+    get/3,
+    get_range/4,
+    get_range/5,
+    get_range_startswith/3,
+    get_range_startswith/4,
+    set/4,
+    wait/1
+]).
+
+
+fold_range(EncryptionContext, DbOrTx, StartKey, EndKey, Fun, Acc) ->
+    fold_range(EncryptionContext, DbOrTx, StartKey, EndKey, Fun, Acc, []).
+
+
+fold_range(EncryptionContext, DbOrTx, StartKey, EndKey, Fun, Acc, Options) ->
+    validate_encryption_context(EncryptionContext),
+    erlfdb:fold_range(DbOrTx, StartKey, EndKey, decrypt_fun(EncryptionContext, Fun), Acc, Options).
+
+
+fold_range_future(EncryptionContext, TxOrSs, StartKey, EndKey, Options) ->
+    validate_encryption_context(EncryptionContext),
+    Future = erlfdb:fold_range_future(TxOrSs, StartKey, EndKey, Options),
+    {aegis_fold_future, EncryptionContext, Future}.
+
+
+fold_range_wait(Tx, {aegis_fold_future, EncryptionContext, Future}, Fun, Acc) ->
+    validate_encryption_context(EncryptionContext),
+    erlfdb:fold_range_wait(Tx, Future, decrypt_fun(EncryptionContext, Fun), Acc).
+
+
+get(EncryptionContext, DbOrTx, Key) ->
+    validate_encryption_context(EncryptionContext),
+    Result = erlfdb:get(DbOrTx, Key),
+    decrypt(EncryptionContext, Key, Result).
+
+
+get_range(EncryptionContext, DbOrTx, StartKey, EndKey) ->
+    get_range(EncryptionContext, DbOrTx, StartKey, EndKey, []).
+
+
+get_range(EncryptionContext, DbOrTx, StartKey, EndKey, Options) ->
+    validate_encryption_context(EncryptionContext),
+    Result = erlfdb:get_range(DbOrTx, StartKey, EndKey, Options),
+    decrypt(EncryptionContext, Result).
+
+
+get_range_startswith(EncryptionContext, DbOrTx, Prefix) ->
+    get_range_startswith(EncryptionContext, DbOrTx, Prefix, []).
+
+
+get_range_startswith(EncryptionContext, DbOrTx, Prefix, Options) ->
+    validate_encryption_context(EncryptionContext),
+    Result = erlfdb:get_range_startswith(DbOrTx, Prefix, Options),
+    decrypt(EncryptionContext, Result).
+
+
+set(EncryptionContext, DbOrTx, Key, Value) ->
+    validate_encryption_context(EncryptionContext),
+    erlfdb:set(DbOrTx, Key, encrypt(EncryptionContext, Key, Value)).
+
+
+wait({aegis_future, EncryptionContext, Future}) ->
+    Value = erlfdb:wait(Future),
+    decrypt(EncryptionContext, Value);
+
+wait({aegis_future, EncryptionContext, Key, Future}) ->
+    Value = erlfdb:wait(Future),
+    decrypt(EncryptionContext, Key, Value);
+
+wait(Result) ->
+    Result.
+
+
+%% Private functions
+
+validate_encryption_context(#{uuid := _UUID}) ->
+    ok;
+validate_encryption_context(_) ->
+    error(invalid_encryption_context).
+
+
+-define(DUMMY_KEY, <<1:256>>).
+
+encrypt(#{uuid := UUID}, Key, Value) ->
+    {CipherText, <<CipherTag:128>>} =
+        ?aes_gcm_encrypt(
+           derive(?DUMMY_KEY, Key),
+           <<0:96>>,
+           UUID,
+           Value),
+    <<1:8, CipherTag:128, CipherText/binary>>.
+
+
+decrypt(EncryptionContext, ?IS_FUTURE = Future) ->
+    decrypt_future(EncryptionContext, Future);
+
+decrypt(EncryptionContext, {Key, Value})
+  when is_binary(Key), is_binary(Value) ->
+    decrypt(EncryptionContext, Key, Value);
+
+decrypt(EncryptionContext, Rows) when is_list(Rows) ->
+    [{Key, decrypt(EncryptionContext, Row)} || {Key, _} = Row <- Rows].
+
+
+decrypt(EncryptionContext, Key, ?IS_FUTURE = Future) ->
+    decrypt_future(EncryptionContext, Key, Future);
+
+decrypt(#{uuid := UUID}, Key, Value) when is_binary(Value) ->
+    <<1:8, CipherTag:128, CipherText/binary>> = Value,
+    Decrypted =
+        ?aes_gcm_decrypt(
+           derive(?DUMMY_KEY, Key),
+           <<0:96>>,
+           UUID,
+           CipherText,
+           <<CipherTag:128>>),
+    case Decrypted of
+        error ->
+            erlang:error(decryption_failed);
+        Decrypted ->
+            Decrypted
+    end;
+
+decrypt(_EncryptionContext, _Key, Value) when not is_binary(Value) ->
+    Value.
+
+
+decrypt_future(EncryptionContext, ?IS_FUTURE = Future) ->
+    {aegis_future, EncryptionContext, Future}.
+
+decrypt_future(EncryptionContext, Key, ?IS_FUTURE = Future) ->
+    {aegis_future, EncryptionContext, Key, Future}.
+
+decrypt_fun(EncryptionContext, Fun) ->
+    fun(Rows, Acc) ->
+            Fun(decrypt(EncryptionContext, Rows), Acc)
+    end.
+
+derive(KEK, KeyMaterial) when bit_size(KEK) == 256 ->
+    PlainText = <<1:16, "aegis", 0:8, KeyMaterial/binary, 256:16>>,
+    <<_:256>> = ?hmac(KEK, PlainText).
diff --git a/src/aegis/test/aegis_basic_test.erl b/src/aegis/test/aegis_basic_test.erl
new file mode 100644
index 0000000..061f724
--- /dev/null
+++ b/src/aegis/test/aegis_basic_test.erl
@@ -0,0 +1,77 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(aegis_basic_test).
+
+-include_lib("eunit/include/eunit.hrl").
+
+-define(DB, #{uuid => <<"foo">>}).
+
+get_set_db_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Key = <<"foo">>,
+    Value = <<"bar">>,
+    ?assertEqual(ok, aegis:set(?DB, Db, Key, Value)),
+    ?assertNotEqual(Value, erlfdb:get(Db, Key)),
+    ?assertEqual(Value, aegis:get(?DB, Db, Key)).
+
+get_set_tx_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Key = <<"foo">>,
+    Value = <<"bar">>,
+    ?assertEqual(ok, aegis:set(?DB, Db, Key, Value)),
+    Tx = erlfdb:create_transaction(Db),
+    Future = aegis:get(?DB, Tx, Key),
+    ?assertEqual(Value, aegis:wait(Future)).
+
+get_range_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Rows = [{<<"foo1">>, <<"bar1">>},
+            {<<"foo2">>, <<"bar2">>},
+            {<<"foo3">>, <<"bar3">>}],
+    [aegis:set(?DB, Db, K, V) || {K, V} <- Rows],
+    ?assertNotEqual(Rows, erlfdb:get_range(Db, <<"foo1">>, <<"foo9">>)),
+    ?assertEqual(Rows, aegis:get_range(?DB, Db, <<"foo1">>, <<"foo9">>)).
+
+get_range_startswith_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Rows = [{<<"foo1">>, <<"bar1">>},
+            {<<"foo2">>, <<"bar2">>},
+            {<<"foo3">>, <<"bar3">>}],
+    [aegis:set(?DB, Db, K, V) || {K, V} <- Rows],
+    ?assertNotEqual(Rows, erlfdb:get_range_startswith(Db, <<"foo">>)),
+    ?assertEqual(Rows, aegis:get_range_startswith(?DB, Db, <<"foo">>)).
+
+fold_range_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Rows = [{<<"foo1">>, <<"bar1">>},
+            {<<"foo2">>, <<"bar2">>},
+            {<<"foo3">>, <<"bar3">>}],
+    {_, Values} = lists:unzip(Rows),
+    [aegis:set(?DB, Db, K, V) || {K, V} <- Rows],
+    Fun = fun(NewRows, Acc) -> [NewRows | Acc] end,
+    ?assertNotEqual(Values, lists:reverse(erlfdb:fold_range(Db, <<"foo1">>, <<"foo9">>, Fun, []))),
+    ?assertEqual(Values, lists:reverse(aegis:fold_range(?DB, Db, <<"foo1">>, <<"foo9">>, Fun, []))).
+
+fold_range_future_test() ->
+    Db = erlfdb_util:get_test_db([empty]),
+    Rows = [{<<"foo1">>, <<"bar1">>},
+            {<<"foo2">>, <<"bar2">>},
+            {<<"foo3">>, <<"bar3">>}],
+    {_, Values} = lists:unzip(Rows),
+    [aegis:set(?DB, Db, K, V) || {K, V} <- Rows],
+    Fun = fun(NewRows, Acc) -> [NewRows | Acc] end,
+    Tx = erlfdb:create_transaction(Db),
+    Future = aegis:fold_range_future(?DB, Tx, <<"foo1">>, <<"foo9">>, []),
+    ?assertEqual(Values, lists:reverse(aegis:fold_range_wait(Tx, Future, Fun, []))).
+
+