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:39 UTC

[couchdb] branch davisp-aegis created (now 6180fc9)

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

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


      at 6180fc9  Example aegis API usage

This branch includes the following new commits:

     new d54b167  Add aegis
     new c8045d6  Cleanup and condense aegis
     new 6180fc9  Example aegis API usage

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[couchdb] 03/03: Example aegis API usage

Posted by da...@apache.org.
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 6180fc90620661d20c5502bc115e8b893d6c1049
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Thu Apr 9 11:17:02 2020 -0500

    Example aegis API usage
---
 src/fabric/src/fabric2_fdb.erl | 54 ++++++++++++++++++++++++------------------
 1 file changed, 31 insertions(+), 23 deletions(-)

diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index 2295a56..9259d9c 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -177,7 +177,7 @@ create(#{} = Db0, Options) ->
         name := DbName,
         tx := Tx,
         layer_prefix := LayerPrefix
-    } = Db = ensure_current(Db0, false),
+    } = Db1 = ensure_current(Db0, false),
 
     DbKey = erlfdb_tuple:pack({?ALL_DBS, DbName}, LayerPrefix),
     HCA = erlfdb_hca:create(erlfdb_tuple:pack({?DB_HCA}, LayerPrefix)),
@@ -220,7 +220,7 @@ create(#{} = Db0, Options) ->
     UserCtx = fabric2_util:get_value(user_ctx, Options, #user_ctx{}),
     Options1 = lists:keydelete(user_ctx, 1, Options),
 
-    Db#{
+    Db2 = Db1#{
         uuid => UUID,
         db_prefix => DbPrefix,
         db_version => DbVersion,
@@ -235,7 +235,8 @@ create(#{} = Db0, Options) ->
         % All other db things as we add features,
 
         db_options => Options1
-    }.
+    },
+    aegis:create(Db2).
 
 
 open(#{} = Db0, Options) ->
@@ -280,14 +281,15 @@ open(#{} = Db0, Options) ->
     },
 
     Db3 = load_config(Db2),
+    Db4 = aegis:open(Db3),
 
-    case {UUID, Db3} of
+    case {UUID, Db4} of
         {undefined, _} -> ok;
         {<<_/binary>>, #{uuid := UUID}} -> ok;
         {<<_/binary>>, #{uuid := _}} -> erlang:error(database_does_not_exist)
     end,
 
-    load_validate_doc_funs(Db3).
+    load_validate_doc_funs(Db4).
 
 
 % Match on `name` in the function head since some non-fabric2 db
@@ -543,7 +545,7 @@ get_all_revs(#{} = Db, DocId) ->
             Key = erlfdb_tuple:unpack(K, DbPrefix),
             Val = erlfdb_tuple:unpack(V),
             fdb_to_revinfo(Key, Val)
-        end, erlfdb:wait(Future))
+        end, aegis:decrypt(Db, erlfdb:wait(Future)))
     end).
 
 
@@ -571,11 +573,12 @@ get_winning_revs_wait(#{} = Db, RangeFuture) ->
         tx := Tx,
         db_prefix := DbPrefix
     } = ensure_current(Db),
-    RevRows = erlfdb:fold_range_wait(Tx, RangeFuture, fun({K, V}, Acc) ->
+    FoldFun = aegis:wrap_fold_fun(Db, fun({K, V}, Acc) ->
         Key = erlfdb_tuple:unpack(K, DbPrefix),
         Val = erlfdb_tuple:unpack(V),
         [fdb_to_revinfo(Key, Val) | Acc]
-    end, []),
+    end),
+    RevRows = erlfdb:fold_range_wait(Tx, RangeFuture, FoldFun, []),
     lists:reverse(RevRows).
 
 
@@ -593,7 +596,8 @@ get_non_deleted_rev(#{} = Db, DocId, RevId) ->
         not_found ->
             not_found;
         Val ->
-            fdb_to_revinfo(BaseKey, erlfdb_tuple:unpack(Val))
+            Decrypted = aegis:decrypt(Db, Key, Value),
+            fdb_to_revinfo(BaseKey, erlfdb_tuple:unpack(Decrypted))
     end.
 
 
@@ -630,9 +634,10 @@ get_doc_body_wait(#{} = Db0, DocId, RevInfo, Future) ->
         rev_path := RevPath
     } = RevInfo,
 
-    RevBodyRows = erlfdb:fold_range_wait(Tx, Future, fun({_K, V}, Acc) ->
+    FoldFun = aegis:wrap_fold_fun(Db, fun({_K, V}, Acc) ->
         [V | Acc]
-    end, []),
+    end),
+    RevBodyRows = erlfdb:fold_range_wait(Tx, Future, FoldFun, []),
     BodyRows = lists:reverse(RevBodyRows),
 
     fdb_to_doc(Db, DocId, RevPos, [Rev | RevPath], BodyRows).
@@ -645,11 +650,11 @@ get_local_doc(#{} = Db0, <<?LOCAL_DOC_PREFIX, _/binary>> = DocId) ->
     } = Db = ensure_current(Db0),
 
     Key = erlfdb_tuple:pack({?DB_LOCAL_DOCS, DocId}, DbPrefix),
-    Rev = erlfdb:wait(erlfdb:get(Tx, Key)),
+    Rev = aegis:decrypt(Db, Key, erlfdb:wait(erlfdb:get(Tx, Key))),
 
     Prefix = erlfdb_tuple:pack({?DB_LOCAL_DOC_BODIES, DocId}, DbPrefix),
     Future = erlfdb:get_range_startswith(Tx, Prefix),
-    Chunks = lists:map(fun({_K, V}) -> V end, erlfdb:wait(Future)),
+    {_, Chunks} = lists:unzip(aegis:decrypt(Db, erlfdb:wait(Future))),
 
     fdb_to_local_doc(Db, DocId, Rev, Chunks).
 
@@ -742,7 +747,7 @@ write_doc(#{} = Db0, Doc, NewWinner0, OldWinner, ToUpdate, ToRemove) ->
     lists:foreach(fun(RI0) ->
         RI = RI0#{winner := false},
         {K, V, undefined} = revinfo_to_fdb(Tx, DbPrefix, DocId, RI),
-        ok = erlfdb:set(Tx, K, V)
+        ok = erlfdb:set(Tx, K, aegis:encrypt(Db, K, V))
     end, ToUpdate),
 
     lists:foreach(fun(RI0) ->
@@ -780,7 +785,7 @@ write_doc(#{} = Db0, Doc, NewWinner0, OldWinner, ToUpdate, ToRemove) ->
         _ ->
             ADKey = erlfdb_tuple:pack({?DB_ALL_DOCS, DocId}, DbPrefix),
             ADVal = erlfdb_tuple:pack(NewRevId),
-            ok = erlfdb:set(Tx, ADKey, ADVal)
+            ok = erlfdb:set(Tx, ADKey, aegis:encrypt(Db, ADKey, ADVal))
     end,
 
     % _changes
@@ -855,7 +860,8 @@ write_local_doc(#{} = Db0, Doc) ->
 
     {LDocKey, LDocVal, NewSize, Rows} = local_doc_to_fdb(Db, Doc),
 
-    {WasDeleted, PrevSize} = case erlfdb:wait(erlfdb:get(Tx, LDocKey)) of
+    PrevVsn = aegis:decrypt(Db, LDocKey, erlfdb:wait(erlfdb:get(Tx, LDocKey))),
+    {WasDeleted, PrevSize} = case  PrevVsn of
         <<255, RevBin/binary>> ->
             case erlfdb_tuple:unpack(RevBin) of
                 {?CURR_LDOC_FORMAT, _Rev, Size} ->
@@ -874,11 +880,13 @@ write_local_doc(#{} = Db0, Doc) ->
             erlfdb:clear(Tx, LDocKey),
             erlfdb:clear_range_startswith(Tx, BPrefix);
         false ->
-            erlfdb:set(Tx, LDocKey, LDocVal),
+            erlfdb:set(Tx, LDocKey, aegis:encrypt(Db, LDocKey, LDocVal)),
             % Make sure to clear the whole range, in case there was a larger
             % document body there before.
             erlfdb:clear_range_startswith(Tx, BPrefix),
-            lists:foreach(fun({K, V}) -> erlfdb:set(Tx, K, V) end, Rows)
+            lists:foreach(fun({K, V}) ->
+                erlfdb:set(Tx, K, aegis:encrypt(Db, K, V))
+            end, Rows)
     end,
 
     case {WasDeleted, Doc#doc.deleted} of
@@ -906,8 +914,8 @@ read_attachment(#{} = Db, DocId, AttId) ->
         not_found ->
             throw({not_found, missing});
         KVs ->
-            Vs = [V || {_K, V} <- KVs],
-            iolist_to_binary(Vs)
+            {_, Chunks} = lists:unzip(aegis:decrypt(Db, KVs)),
+            iolist_to_binary(Chunks)
     end.
 
 
@@ -921,11 +929,11 @@ write_attachment(#{} = Db, DocId, Data) when is_binary(Data) ->
     Chunks = chunkify_binary(Data),
 
     IdKey = erlfdb_tuple:pack({?DB_ATT_NAMES, DocId, AttId}, DbPrefix),
-    ok = erlfdb:set(Tx, IdKey, <<>>),
+    ok = erlfdb:set(Tx, IdKey, aegis:encrypt(Db, IdKey, <<>>)),
 
     lists:foldl(fun(Chunk, ChunkId) ->
         AttKey = erlfdb_tuple:pack({?DB_ATTS, DocId, AttId, ChunkId}, DbPrefix),
-        ok = erlfdb:set(Tx, AttKey, Chunk),
+        ok = erlfdb:set(Tx, AttKey, aegis:encrypt(Db, AttKey, Chunk)),
         ChunkId + 1
     end, 0, Chunks),
     {ok, AttId}.
@@ -979,7 +987,7 @@ fold_range(Tx, FAcc) ->
         ok = erlfdb:set_option(Tx, disallow_writes)
     end,
     Opts = [{limit, Limit} | BaseOpts],
-    Callback = fun fold_range_cb/2,
+    Callback = aegis:wrap_fold_fun(Db, fun fold_range_cb/2),
     try
         #fold_acc{
             user_acc = FinalUserAcc


[couchdb] 02/03: Cleanup and condense aegis

Posted by da...@apache.org.
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 c8045d659779ff3dce4ef39e1c5769d1008faa40
Author: Paul J. Davis <pa...@gmail.com>
AuthorDate: Thu Apr 9 11:16:51 2020 -0500

    Cleanup and condense aegis
---
 src/aegis/src/aegis.app.src |  33 ++++---
 src/aegis/src/aegis.erl     | 208 ++++++++++++++++----------------------------
 2 files changed, 90 insertions(+), 151 deletions(-)

diff --git a/src/aegis/src/aegis.app.src b/src/aegis/src/aegis.app.src
index eb3018f..e052323 100644
--- a/src/aegis/src/aegis.app.src
+++ b/src/aegis/src/aegis.app.src
@@ -10,20 +10,19 @@
 % 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, []}
- ]}.
+{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
index 5937b4c..9b4778d 100644
--- a/src/aegis/src/aegis.erl
+++ b/src/aegis/src/aegis.erl
@@ -12,9 +12,6 @@
 
 -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)).
@@ -27,11 +24,12 @@
 -ifdef(OTP_RELEASE).
 -if(?OTP_RELEASE >= 22).
 -undef(hmac).
--define(hmac(Key, PlainText), crypto:mac(hmac, sha256, Key, PlainText)).
 -undef(aes_gcm_encrypt).
+-undef(aes_gcm_decrypt).
+
+-define(hmac(Key, PlainText), crypto:mac(hmac, sha256, Key, PlainText)).
 -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)).
@@ -39,147 +37,89 @@
 -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
+    init/1,
+    encrypt/3,
+    decrypt/3,
+    decrypt/2,
+    wrap_fold/2
 ]).
 
 
-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),
+-define(AEGIS_KEY_DOOHICKIES, 254).
+
+
+create(#{} = Db) ->
+    #{
+        tx = Tx,
+        db_prefix = DbPrefix
+    } = Db,
+    DbKeyThinger = erlfdb_tuple:pack({?AEGIS_KEY_DOOHICKIES}, DbPrefix),
+    MyThingAMaJig = oooh_look_at_my_fancy_math(UUID, MathStuff),
+    ok = erlfdb:set(Tx, DbKeyThinger, MyThingAMaJig),
+    Db#{
+        aegis_ctx = MyThingAMaJig
+    }.
+
+
+open(#{} = Db) ->
+    #{
+        tx = Tx,
+        db_prefix = DbPrefix
+    } = Db,
+    DbKeyThinger = erlfdb_tuple:pack({?AEGIS_KEY_DOOHICKIES}, DbPrefix),
+    MyLookupThing = erlfdb:wait(erlfdb:get(Tx, DbKeyThinger)),
+    Db#{
+        aegis_ctx => MyLookupThing
+    }.
+
+
+encrypt(#{} = Db, Key, Value) ->
+    #{
+        aegis_ctx = Ctx,
+        uuid := UUID
+    } = Db,
+    DerivedKey = derive(?DUMMY_KEY, Key),
+    {CipherText, <<CipherTag:128>>} = ?aes_gcm_encrypt(
+            Derived,
+            <<0:96>>,
+            UUID,
+            Value
+        ),
     <<1:8, CipherTag:128, CipherText/binary>>.
 
 
-decrypt(EncryptionContext, ?IS_FUTURE = Future) ->
-    decrypt_future(EncryptionContext, Future);
+decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
+    #{
+        aegis_ctx = Ctx,
+        uuid := UUID
+    } = Db,
+    <<1:8, CipherTag:128, CipherText/binary>> = Value,
+    Derived = derive(?DUMMY_KEY, Key),
+    Decrypted = ?aes_gcm_decrypt(
+            Derived,
+            <<0:96>>,
+            UUID,
+            CipherText,
+            <<CipherTag:128>>
+        ),
+    if Decrypted /= error -> Decrypted; true ->
+        erlang:error(decryption_failed)
+    end.
 
-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(Db, {Key, Value}) ->
+    {Key, decrypt(Db, Key, Value)};
 
+decrypt(Db, Rows) when is_list(Rows) ->
+    lists:map(fun({Key, Value}) ->
+        decrypt(Db, {Key, Value})
+    end, 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.
+wrap_fold_fun(Db, Fun) when is_function(Fun, 2)->
+    fun({Key, Val}, Acc) -> Fun(decrypt(Db, {Key, Value}), Acc) end.
+
 
 derive(KEK, KeyMaterial) when bit_size(KEK) == 256 ->
     PlainText = <<1:16, "aegis", 0:8, KeyMaterial/binary, 256:16>>,


[couchdb] 01/03: Add aegis

Posted by da...@apache.org.
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, []))).
+
+