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

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


The following commit(s) were added to refs/heads/aegis by this push:
     new b63f074  Make aegis into app and add key cache server (#2792)
b63f074 is described below

commit b63f0746c9924c214120c487eb40bf0b538dbce2
Author: Eric Avdey <[email protected]>
AuthorDate: Mon Apr 27 09:15:24 2020 -0300

    Make aegis into app and add key cache server (#2792)
    
    * Fix typo in configure
    
    * Move rebar.config.script from couch to aegis
    
    * Make aegis into app and add key cache server
    
    * Make fabric depend on aegis
    
    * Fail tests on gen_server timeout
    
    * Return error if can't unwrap a key
    
    * Extract aegis_keywrap parts in shim of key manager
    
    * Formatting: move private functions into own section
    
    * Make encrypt/decrypt workers to reply client directly
    
    * Rename unwrap_key to maybe_rewrap_key to clarify fun propose
    
    * Rename clients to openers
    
    * Refactoring: extract maybe_spawn_unwrapper into own function
    
    * Address review comments
    
    - Rename aegis_key_cache to aegis_server
    - Move crypto into workers
    - Add aegis_server public API
    - Define aegis_key_manager behaviour
    - Fix error messages
    - Remove part with incomplete functionality
    - Remove defensive programming
    
    * Change rebar deps build order to allow aegis find fabric2.hrl header file
    
    * Pass db options to generate_key and rename WrappedKey to AegisConfig
    
    * Support disabling encryption per database on key manager level
    
    Allow key manager to return `{ok false}` on generate key request.
    
    Treat this as a flag to perf db disabled encryption and passthrough
    encrypt and decrypt without calling server in this case.
    
    * Store unwrappers Ref as a control for DbKey legitimacy
    
    * Convert aegis_key_manager into gen server
    
    Convert aegis_key_manager into gen_server and make it keep private state
    for key manager callback module.
    
    Rename aegis_file_key_manager into aegis_example_key_manager
    to clarify intent and adapt it to new aegis_key_manager behaviour
    
    * Make example key manager use pbkdf2 to derive root key
    
    * Change key manager response on disabled encryption to `false`.
    
    * Remove example key manager, add noop key manager, make it default
    
    * Switch to db UUID for cache's key
    
    * Fix aegis_server multidb test, allow waiters to properly accumulate
    
    * Simplify aegis design
    
    - Rename create and open method to init_db and open_db across all the 
modules
    - Remove all functionality from aegis_key_manager.
      Make it just a behaviour definition module
    - Make ?AEGIS_KEY_MANAGER handle fdb operations
    - Remove all machinery from aegis_server, just make it own ets cache
    - Make do_encode and do_decode functions private
    
    Co-authored-by: Robert Newson <[email protected]>
    
    * Use protected ets with UUID only to check key availability
    
    Keep named protected ets table with UUIDs only in sync with cache
    for availability check to save gen_server's round-trip
    
    * Pass encryption/decryption errors back to chttpd
    
    * Change arity for open_db
    
    Co-authored-by: Robert Newson <[email protected]>
---
 configure                                          |   2 +-
 rebar.config.script                                |   2 +-
 rel/reltool.config                                 |   1 +
 src/aegis/rebar.config.script                      |  35 +++
 src/aegis/src/aegis.app.src                        |   5 +-
 src/aegis/src/aegis.erl                            |  94 ++-----
 src/aegis/src/{aegis.app.src => aegis_app.erl}     |  33 ++-
 .../src/{aegis.app.src => aegis_key_manager.erl}   |  29 +--
 .../{aegis.app.src => aegis_noop_key_manager.erl}  |  38 +--
 src/aegis/src/aegis_server.erl                     | 275 +++++++++++++++++++++
 .../src/fabric.app.src => aegis/src/aegis_sup.erl} |  55 +++--
 src/aegis/test/aegis_server_test.erl               | 165 +++++++++++++
 src/chttpd/src/chttpd.erl                          |   8 +
 src/couch/rebar.config.script                      |  11 +-
 src/fabric/src/fabric.app.src                      |   3 +-
 src/fabric/src/fabric2_fdb.erl                     |   4 +-
 16 files changed, 589 insertions(+), 171 deletions(-)

diff --git a/configure b/configure
index 5bd40d3..f22233d 100755
--- a/configure
+++ b/configure
@@ -259,7 +259,7 @@ cat > $rootdir/config.erl << EOF
 {with_curl, $WITH_CURL}.
 {with_proper, $WITH_PROPER}.
 {erlang_md5, $ERLANG_MD5}.
-{aegis_key_manager, "$AEGIS_KEY_MANAGER"},
+{aegis_key_manager, "$AEGIS_KEY_MANAGER"}.
 {spidermonkey_version, "$SM_VSN"}.
 EOF
 
diff --git a/rebar.config.script b/rebar.config.script
index 118a99e..a94e070 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -114,7 +114,6 @@ 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",
@@ -136,6 +135,7 @@ SubDirs = [
     "src/ddoc_cache",
     "src/dreyfus",
     "src/fabric",
+    "src/aegis",
     "src/couch_jobs",
     "src/couch_expiring_cache",
     "src/global_changes",
diff --git a/rel/reltool.config b/rel/reltool.config
index 1e64a80..b59c95f 100644
--- a/rel/reltool.config
+++ b/rel/reltool.config
@@ -27,6 +27,7 @@
         syntax_tools,
         xmerl,
         %% couchdb
+        aegis,
         b64url,
         bear,
         chttpd,
diff --git a/src/aegis/rebar.config.script b/src/aegis/rebar.config.script
new file mode 100644
index 0000000..ef148bf
--- /dev/null
+++ b/src/aegis/rebar.config.script
@@ -0,0 +1,35 @@
+% 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.
+
+
+CouchConfig = case filelib:is_file(os:getenv("COUCHDB_CONFIG")) of
+    true ->
+        {ok, Result} = file:consult(os:getenv("COUCHDB_CONFIG")),
+        Result;
+    false ->
+        []
+end.
+
+AegisKeyManager = case lists:keyfind(aegis_key_manager, 1, CouchConfig) of
+    {aegis_key_manager, Module} when Module /= "" ->
+        list_to_atom(Module);
+    _ ->
+        aegis_noop_key_manager
+end,
+
+CurrentOpts = case lists:keyfind(erl_opts, 1, CONFIG) of
+    {erl_opts, Opts} -> Opts;
+    false -> []
+end,
+
+AegisOpts = {d, 'AEGIS_KEY_MANAGER', AegisKeyManager},
+lists:keystore(erl_opts, 1, CONFIG, {erl_opts, [AegisOpts | CurrentOpts]}).
diff --git a/src/aegis/src/aegis.app.src b/src/aegis/src/aegis.app.src
index 51b608d..deb1526 100644
--- a/src/aegis/src/aegis.app.src
+++ b/src/aegis/src/aegis.app.src
@@ -14,12 +14,15 @@
  [
   {description, "If it's good enough for Zeus, it's good enough for CouchDB"},
   {vsn, git},
+  {mod, {aegis_app, []}},
+  {registered, [
+    aegis_server
+  ]},
   {applications,
    [kernel,
     stdlib,
     crypto,
     couch_log,
-    base64,
     erlfdb
    ]},
   {env,[]},
diff --git a/src/aegis/src/aegis.erl b/src/aegis/src/aegis.erl
index dc8271f..e8a0b4b 100644
--- a/src/aegis/src/aegis.erl
+++ b/src/aegis/src/aegis.erl
@@ -11,18 +11,15 @@
 % the License.
 
 -module(aegis).
--include("aegis.hrl").
 -include_lib("fabric/include/fabric2.hrl").
 
-%% TODO - get from key manager
--define(ROOT_KEY, <<1:256>>).
 
 -define(WRAPPED_KEY, {?DB_AEGIS, 1}).
 
 
 -export([
-    create/2,
-    open/2,
+    init_db/2,
+    open_db/1,
 
     decrypt/2,
     decrypt/3,
@@ -30,64 +27,27 @@
     wrap_fold_fun/2
 ]).
 
-create(#{} = Db, Options) ->
-    #{
-        tx := Tx,
-        db_prefix := DbPrefix
-    } = Db,
-
-    % Generate new key
-    DbKey = crypto:strong_rand_bytes(32),
-
-    % protect it with root key
-    WrappedKey = aegis_keywrap:key_wrap(?ROOT_KEY, DbKey),
-
-    % And store it
-    FDBKey = erlfdb_tuple:pack(?WRAPPED_KEY, DbPrefix),
-    ok = erlfdb:set(Tx, FDBKey, WrappedKey),
-
+init_db(#{} = Db, Options) ->
     Db#{
-        aegis => DbKey
+        is_encrypted => aegis_server:init_db(Db, Options)
     }.
 
 
-open(#{} = Db, Options) ->
-    #{
-        tx := Tx,
-        db_prefix := DbPrefix
-    } = Db,
-
-    % Fetch wrapped key
-    FDBKey = erlfdb_tuple:pack(?WRAPPED_KEY, DbPrefix),
-    WrappedKey = erlfdb:wait(erlfdb:get(Tx, FDBKey)),
-
-    % Unwrap it
-    DbKey = aegis_keywrap:key_unwrap(?ROOT_KEY, WrappedKey),
-
+open_db(#{} = Db) ->
     Db#{
-        aegis => DbKey
+        is_encrypted => aegis_server:open_db(Db)
     }.
 
 
 encrypt(#{} = _Db, _Key, <<>>) ->
     <<>>;
 
-encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
-    #{
-        uuid := UUID,
-        aegis := DbKey
-    } = Db,
-
-    EncryptionKey = crypto:strong_rand_bytes(32),
-    <<WrappedKey:320>> = aegis_keywrap:key_wrap(DbKey, EncryptionKey),
+encrypt(#{is_encrypted := false}, _Key, Value) when is_binary(Value) ->
+    Value;
 
-    {CipherText, <<CipherTag:128>>} =
-        ?aes_gcm_encrypt(
-           EncryptionKey,
-           <<0:96>>,
-           <<UUID/binary, 0:8, Key/binary>>,
-           Value),
-    <<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>>.
+encrypt(#{is_encrypted := true} = Db, Key, Value)
+        when is_binary(Key), is_binary(Value) ->
+    aegis_server:encrypt(Db, Key, Value).
 
 
 decrypt(#{} = Db, Rows) when is_list(Rows) ->
@@ -98,32 +58,12 @@ decrypt(#{} = Db, Rows) when is_list(Rows) ->
 decrypt(#{} = _Db, _Key, <<>>) ->
     <<>>;
 
-decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
-    #{
-        uuid := UUID,
-        aegis := DbKey
-    } = Db,
-
-    case Value of
-        <<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>> ->
-            case aegis_keywrap:key_unwrap(DbKey, <<WrappedKey:320>>) of
-                fail ->
-                    erlang:error(decryption_failed);
-                DecryptionKey ->
-                    Decrypted =
-                    ?aes_gcm_decrypt(
-                        DecryptionKey,
-                        <<0:96>>,
-                        <<UUID/binary, 0:8, Key/binary>>,
-                        CipherText,
-                        <<CipherTag:128>>),
-                    if Decrypted /= error -> Decrypted; true ->
-                        erlang:error(decryption_failed)
-                    end
-            end;
-        _ ->
-            erlang:error(not_ciphertext)
-    end.
+decrypt(#{is_encrypted := false}, _Key, Value) when is_binary(Value) ->
+    Value;
+
+decrypt(#{is_encrypted := true} = Db, Key, Value)
+        when is_binary(Key), is_binary(Value) ->
+    aegis_server:decrypt(Db, Key, Value).
 
 
 wrap_fold_fun(Db, Fun) when is_function(Fun, 2) ->
diff --git a/src/aegis/src/aegis.app.src b/src/aegis/src/aegis_app.erl
similarity index 64%
copy from src/aegis/src/aegis.app.src
copy to src/aegis/src/aegis_app.erl
index 51b608d..4a5a11f 100644
--- a/src/aegis/src/aegis.app.src
+++ b/src/aegis/src/aegis_app.erl
@@ -10,22 +10,17 @@
 % 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},
-  {applications,
-   [kernel,
-    stdlib,
-    crypto,
-    couch_log,
-    base64,
-    erlfdb
-   ]},
-  {env,[]},
-  {modules, []},
-  {maintainers, []},
-  {licenses, []},
-  {links, []}
- ]
-}.
+-module(aegis_app).
+
+-behaviour(application).
+
+
+-export([start/2, stop/1]).
+
+
+start(_StartType, _StartArgs) ->
+    aegis_sup:start_link().
+
+
+stop(_State) ->
+    ok.
diff --git a/src/aegis/src/aegis.app.src b/src/aegis/src/aegis_key_manager.erl
similarity index 64%
copy from src/aegis/src/aegis.app.src
copy to src/aegis/src/aegis_key_manager.erl
index 51b608d..aa9e342 100644
--- a/src/aegis/src/aegis.app.src
+++ b/src/aegis/src/aegis_key_manager.erl
@@ -10,22 +10,13 @@
 % 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},
-  {applications,
-   [kernel,
-    stdlib,
-    crypto,
-    couch_log,
-    base64,
-    erlfdb
-   ]},
-  {env,[]},
-  {modules, []},
-  {maintainers, []},
-  {licenses, []},
-  {links, []}
- ]
-}.
+-module(aegis_key_manager).
+
+
+
+-callback init_db(
+    Db :: #{},
+    DbOptions :: list()) -> {ok, binary()} | false.
+
+
+-callback open_db(Db :: #{}) -> {ok, binary()} | false.
diff --git a/src/aegis/src/aegis.app.src 
b/src/aegis/src/aegis_noop_key_manager.erl
similarity index 64%
copy from src/aegis/src/aegis.app.src
copy to src/aegis/src/aegis_noop_key_manager.erl
index 51b608d..2b61f1d 100644
--- a/src/aegis/src/aegis.app.src
+++ b/src/aegis/src/aegis_noop_key_manager.erl
@@ -10,22 +10,22 @@
 % 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},
-  {applications,
-   [kernel,
-    stdlib,
-    crypto,
-    couch_log,
-    base64,
-    erlfdb
-   ]},
-  {env,[]},
-  {modules, []},
-  {maintainers, []},
-  {licenses, []},
-  {links, []}
- ]
-}.
+-module(aegis_noop_key_manager).
+
+
+-behaviour(aegis_key_manager).
+
+
+-export([
+    init_db/2,
+    open_db/1
+]).
+
+
+
+init_db(#{} = _Db, _Options) ->
+    false.
+
+
+open_db(#{} = _Db) ->
+    false.
diff --git a/src/aegis/src/aegis_server.erl b/src/aegis/src/aegis_server.erl
new file mode 100644
index 0000000..be8202c
--- /dev/null
+++ b/src/aegis/src/aegis_server.erl
@@ -0,0 +1,275 @@
+% 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_server).
+
+-behaviour(gen_server).
+
+-vsn(1).
+
+
+-include("aegis.hrl").
+
+
+%% aegis_server API
+-export([
+    start_link/0,
+    init_db/2,
+    open_db/1,
+    encrypt/3,
+    decrypt/3
+]).
+
+%% gen_server callbacks
+-export([
+    init/1,
+    terminate/2,
+    handle_call/3,
+    handle_cast/2,
+    handle_info/2,
+    code_change/3
+]).
+
+
+
+-define(KEY_CHECK, aegis_key_check).
+-define(INIT_TIMEOUT, 60000).
+-define(TIMEOUT, 10000).
+
+
+-record(entry, {uuid, encryption_key}).
+
+
+start_link() ->
+    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+
+-spec init_db(Db :: #{}, Options :: list()) -> boolean().
+init_db(#{uuid := UUID} = Db, Options) ->
+    process_flag(sensitive, true),
+
+    case ?AEGIS_KEY_MANAGER:init_db(Db, Options) of
+        {ok, DbKey} ->
+            gen_server:call(?MODULE, {insert_key, UUID, DbKey}),
+            true;
+        false ->
+            false
+    end.
+
+
+-spec open_db(Db :: #{}) -> boolean().
+open_db(#{} = Db) ->
+    process_flag(sensitive, true),
+
+    case do_open_db(Db) of
+        {ok, _DbKey} ->
+            true;
+        false ->
+            false
+    end.
+
+
+-spec encrypt(Db :: #{}, Key :: binary(), Value :: binary()) -> binary().
+encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
+    #{
+        uuid := UUID
+    } = Db,
+
+    case ets:member(?KEY_CHECK, UUID) of
+        true ->
+            case gen_server:call(?MODULE, {encrypt, Db, Key, Value}) of
+                CipherText when is_binary(CipherText) ->
+                    CipherText;
+                {error, {_Tag, {_C_FileName,_LineNumber}, _Desc} = Reason} ->
+                    couch_log:error("aegis encryption failure: ~p ", [Reason]),
+                    erlang:error(decryption_failed);
+                {error, Reason} ->
+                    erlang:error(Reason)
+            end;
+        false ->
+            process_flag(sensitive, true),
+
+            {ok, DbKey} = do_open_db(Db),
+            do_encrypt(DbKey, Db, Key, Value)
+    end.
+
+
+-spec decrypt(Db :: #{}, Key :: binary(), Value :: binary()) -> binary().
+decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
+    #{
+        uuid := UUID
+    } = Db,
+
+    case ets:member(?KEY_CHECK, UUID) of
+        true ->
+            case gen_server:call(?MODULE, {decrypt, Db, Key, Value}) of
+                PlainText when is_binary(PlainText) ->
+                    PlainText;
+                {error, {_Tag, {_C_FileName,_LineNumber}, _Desc} = Reason} ->
+                    couch_log:error("aegis decryption failure: ~p ", [Reason]),
+                    erlang:error(decryption_failed);
+                {error, Reason} ->
+                    erlang:error(Reason)
+            end;
+        false ->
+            process_flag(sensitive, true),
+
+            {ok, DbKey} = do_open_db(Db),
+            do_decrypt(DbKey, Db, Key, Value)
+    end.
+
+
+%% gen_server functions
+
+init([]) ->
+    process_flag(sensitive, true),
+    Cache = ets:new(?MODULE, [set, private, {keypos, #entry.uuid}]),
+    ets:new(?KEY_CHECK, [named_table, protected, {read_concurrency, true}]),
+
+    St = #{
+        cache => Cache
+    },
+    {ok, St, ?INIT_TIMEOUT}.
+
+
+terminate(_Reason, _St) ->
+    ok.
+
+
+handle_call({insert_key, UUID, DbKey}, _From, #{cache := Cache} = St) ->
+    ok = insert(Cache, UUID, DbKey),
+    {reply, ok, St, ?TIMEOUT};
+
+handle_call({encrypt, #{uuid := UUID} = Db, Key, Value}, From, St) ->
+    #{
+        cache := Cache
+    } = St,
+
+    {ok, DbKey} = lookup(Cache, UUID),
+
+    erlang:spawn(fun() ->
+        process_flag(sensitive, true),
+        try
+            do_encrypt(DbKey, Db, Key, Value)
+        of
+            Resp ->
+                gen_server:reply(From, Resp)
+        catch
+            _:Error ->
+                gen_server:reply(From, {error, Error})
+        end
+    end),
+
+    {noreply, St, ?TIMEOUT};
+
+handle_call({decrypt, #{uuid := UUID} = Db, Key, Value}, From, St) ->
+    #{
+        cache := Cache
+    } = St,
+
+    {ok, DbKey} = lookup(Cache, UUID),
+
+    erlang:spawn(fun() ->
+        process_flag(sensitive, true),
+        try
+            do_decrypt(DbKey, Db, Key, Value)
+        of
+            Resp ->
+                gen_server:reply(From, Resp)
+        catch
+            _:Error ->
+                gen_server:reply(From, {error, Error})
+        end
+    end),
+
+    {noreply, St, ?TIMEOUT};
+
+handle_call(_Msg, _From, St) ->
+    {noreply, St}.
+
+
+handle_cast(_Msg, St) ->
+    {noreply, St}.
+
+
+handle_info(_Msg, St) ->
+    {noreply, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+    {ok, St}.
+
+
+%% private functions
+
+do_open_db(#{uuid := UUID} = Db) ->
+    case ?AEGIS_KEY_MANAGER:open_db(Db) of
+        {ok, DbKey} ->
+            gen_server:call(?MODULE, {insert_key, UUID, DbKey}),
+            {ok, DbKey};
+        false ->
+            false
+    end.
+
+
+do_encrypt(DbKey, #{uuid := UUID}, Key, Value) ->
+    EncryptionKey = crypto:strong_rand_bytes(32),
+    <<WrappedKey:320>> = aegis_keywrap:key_wrap(DbKey, EncryptionKey),
+
+    {CipherText, <<CipherTag:128>>} =
+        ?aes_gcm_encrypt(
+           EncryptionKey,
+           <<0:96>>,
+           <<UUID/binary, 0:8, Key/binary>>,
+           Value),
+    <<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>>.
+
+
+do_decrypt(DbKey, #{uuid := UUID}, Key, Value) ->
+    case Value of
+        <<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>> ->
+            case aegis_keywrap:key_unwrap(DbKey, <<WrappedKey:320>>) of
+                fail ->
+                    erlang:error(decryption_failed);
+                DecryptionKey ->
+                    Decrypted =
+                    ?aes_gcm_decrypt(
+                        DecryptionKey,
+                        <<0:96>>,
+                        <<UUID/binary, 0:8, Key/binary>>,
+                        CipherText,
+                        <<CipherTag:128>>),
+                    if Decrypted /= error -> Decrypted; true ->
+                        erlang:error(decryption_failed)
+                    end
+            end;
+        _ ->
+            erlang:error(not_ciphertext)
+    end.
+
+
+%% cache functions
+
+insert(Cache, UUID, DbKey) ->
+    Entry = #entry{uuid = UUID, encryption_key = DbKey},
+    true = ets:insert(Cache, Entry),
+    true = ets:insert(?KEY_CHECK, {UUID, true}),
+    ok.
+
+
+lookup(Cache, UUID) ->
+    case ets:lookup(Cache, UUID) of
+        [#entry{uuid = UUID, encryption_key = DbKey}] ->
+            {ok, DbKey};
+        [] ->
+            {error, not_found}
+    end.
diff --git a/src/fabric/src/fabric.app.src b/src/aegis/src/aegis_sup.erl
similarity index 52%
copy from src/fabric/src/fabric.app.src
copy to src/aegis/src/aegis_sup.erl
index 0538b19..6d3ee83 100644
--- a/src/fabric/src/fabric.app.src
+++ b/src/aegis/src/aegis_sup.erl
@@ -10,24 +10,37 @@
 % License for the specific language governing permissions and limitations under
 % the License.
 
-{application, fabric, [
-    {description, "Routing and proxying layer for CouchDB cluster"},
-    {vsn, git},
-    {mod, {fabric2_app, []}},
-    {registered, [
-        fabric_server
-    ]},
-    {applications, [
-        kernel,
-        stdlib,
-        config,
-        couch_epi,
-        couch,
-        ctrace,
-        rexi,
-        mem3,
-        couch_log,
-        couch_stats,
-        erlfdb
-    ]}
-]}.
+-module(aegis_sup).
+
+-behaviour(supervisor).
+
+-vsn(1).
+
+
+-export([
+    start_link/0
+]).
+
+-export([
+    init/1
+]).
+
+
+start_link() ->
+    supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+
+init([]) ->
+    Flags = #{
+        strategy => one_for_one,
+        intensity => 5,
+        period => 10
+    },
+    Children = [
+        #{
+            id => aegis_server,
+            start => {aegis_server, start_link, []},
+            shutdown => 5000
+        }
+    ],
+    {ok, {Flags, Children}}.
diff --git a/src/aegis/test/aegis_server_test.erl 
b/src/aegis/test/aegis_server_test.erl
new file mode 100644
index 0000000..0f23a3f
--- /dev/null
+++ b/src/aegis/test/aegis_server_test.erl
@@ -0,0 +1,165 @@
+% 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_server_test).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+
+-define(DB, #{uuid => <<0:64>>}).
+-define(VALUE, <<0:8>>).
+-define(ENCRYPTED, <<1,155,242,89,190,54,112,151,18,145,25,251,217,
+    49,147,125,14,162,146,201,189,100,232,38,239,111,163,84,25,60,
+    147,167,237,107,24,204,171,232,227,16,72,203,101,118,150,252,
+    204,80,245,66,98,213,223,63,111,105,101,154>>).
+-define(TIMEOUT, 10000).
+
+
+
+basic_test_() ->
+    {
+        foreach,
+        fun setup/0,
+        fun teardown/1,
+        [
+            {"init_db returns true when encryption enabled",
+            {timeout, ?TIMEOUT, fun test_init_db/0}},
+            {"open_db returns true when encryption enabled",
+            {timeout, ?TIMEOUT, fun test_open_db/0}},
+            {"init_db caches key",
+            {timeout, ?TIMEOUT, fun test_init_db_cache/0}},
+            {"open_db caches key",
+            {timeout, ?TIMEOUT, fun test_open_db_cache/0}},
+            {"encrypt fetches and caches key when it's missing",
+            {timeout, ?TIMEOUT, fun test_encrypt_cache/0}},
+            {"decrypt fetches and caches key when it's missing",
+            {timeout, ?TIMEOUT, fun test_decrypt_cache/0}}
+        ]
+    }.
+
+
+setup() ->
+    Ctx = test_util:start_couch([fabric]),
+    meck:new([?AEGIS_KEY_MANAGER], [passthrough]),
+    ok = meck:expect(?AEGIS_KEY_MANAGER, init_db, 2, {ok, <<0:256>>}),
+    ok = meck:expect(?AEGIS_KEY_MANAGER, open_db, 1, {ok, <<0:256>>}),
+    Ctx.
+
+
+teardown(Ctx) ->
+    meck:unload(),
+    test_util:stop_couch(Ctx).
+
+
+test_init_db() ->
+    ?assert(aegis_server:init_db(?DB, [])),
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, init_db, 2)).
+
+
+test_open_db() ->
+    ?assert(aegis_server:open_db(?DB)),
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_init_db_cache() ->
+    ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, init_db, 2)),
+
+    ?assert(aegis_server:init_db(?DB, [])),
+
+    lists:foreach(fun(I) ->
+        Encrypted = aegis_server:encrypt(?DB, <<I:64>>, ?VALUE),
+        ?assertNotEqual(?VALUE, Encrypted),
+        ?assertMatch(<<1:8, _/binary>>, Encrypted)
+    end, lists:seq(1, 12)),
+
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, init_db, 2)).
+
+
+test_open_db_cache() ->
+    ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+    ?assert(aegis_server:open_db(?DB)),
+
+    lists:foreach(fun(I) ->
+        Encrypted = aegis_server:encrypt(?DB, <<I:64>>, ?VALUE),
+        ?assertNotEqual(?VALUE, Encrypted),
+        ?assertMatch(<<1:8, _/binary>>, Encrypted)
+    end, lists:seq(1, 12)),
+
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_encrypt_cache() ->
+    ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+    Encrypted = aegis_server:encrypt(?DB, <<1:64>>, ?VALUE),
+    ?assertNotEqual(?VALUE, Encrypted),
+    ?assertMatch(<<1:8, _/binary>>, Encrypted),
+
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_decrypt_cache() ->
+    ?assertEqual(0, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)),
+
+    Decrypted = aegis_server:decrypt(?DB, <<1:64>>, ?ENCRYPTED),
+    ?assertEqual(<<0>>, Decrypted),
+
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+
+disabled_test_() ->
+    {
+        foreach,
+        fun() ->
+            Ctx = setup(),
+            ok = meck:delete(?AEGIS_KEY_MANAGER, init_db, 2),
+            ok = meck:expect(?AEGIS_KEY_MANAGER, init_db, 2, false),
+            ok = meck:delete(?AEGIS_KEY_MANAGER, open_db, 1),
+            ok = meck:expect(?AEGIS_KEY_MANAGER, open_db, 1, false),
+            Ctx
+        end,
+        fun teardown/1,
+        [
+            {"init_db returns false when encryptions disabled",
+            {timeout, ?TIMEOUT, fun test_disabled_init_db/0}},
+            {"open_db returns false when encryptions disabled",
+            {timeout, ?TIMEOUT, fun test_disabled_open_db/0}},
+            {"pass through on encrypt when encryption disabled",
+            {timeout, ?TIMEOUT, fun test_disabled_encrypt/0}},
+            {"pass through on decrypt when encryption disabled",
+            {timeout, ?TIMEOUT, fun test_disabled_decrypt/0}}
+        ]
+    }.
+
+
+test_disabled_init_db() ->
+    ?assertNot(aegis_server:init_db(?DB, [])),
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, init_db, 2)).
+
+
+test_disabled_open_db() ->
+    ?assertNot(aegis_server:open_db(?DB)),
+    ?assertEqual(1, meck:num_calls(?AEGIS_KEY_MANAGER, open_db, 1)).
+
+
+test_disabled_encrypt() ->
+    Db = ?DB#{is_encrypted => aegis_server:open_db(?DB)},
+    Encrypted = aegis:encrypt(Db, <<1:64>>, ?VALUE),
+    ?assertEqual(?VALUE, Encrypted).
+
+
+test_disabled_decrypt() ->
+    Db = ?DB#{is_encrypted => aegis_server:open_db(?DB)},
+    Decrypted = aegis:decrypt(Db, <<1:64>>, ?ENCRYPTED),
+    ?assertEqual(?ENCRYPTED, Decrypted).
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index 2641007..04e2bc4 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -356,6 +356,10 @@ catch_error(HttpReq, throw, Error) ->
     send_error(HttpReq, Error);
 catch_error(HttpReq, error, database_does_not_exist) ->
     send_error(HttpReq, database_does_not_exist);
+catch_error(HttpReq, error, decryption_failed) ->
+    send_error(HttpReq, decryption_failed);
+catch_error(HttpReq, error, not_ciphertext) ->
+    send_error(HttpReq, not_ciphertext);
 catch_error(HttpReq, Tag, Error) ->
     Stack = erlang:get_stacktrace(),
     % TODO improve logging and metrics collection for client disconnects
@@ -964,6 +968,10 @@ error_info(not_implemented) ->
 error_info(timeout) ->
     {500, <<"timeout">>, <<"The request could not be processed in a reasonable"
         " amount of time.">>};
+error_info(decryption_failed) ->
+    {500, <<"decryption_failed">>, <<"Decryption failed">>};
+error_info(not_ciphertext) ->
+    {500, <<"not_ciphertext">>, <<"Not Ciphertext">>};
 error_info({service_unavailable, Reason}) ->
     {503, <<"service unavailable">>, Reason};
 error_info({timeout, _Reason}) ->
diff --git a/src/couch/rebar.config.script b/src/couch/rebar.config.script
index e281eab..91e24d9 100644
--- a/src/couch/rebar.config.script
+++ b/src/couch/rebar.config.script
@@ -92,15 +92,6 @@ MD5Config = case lists:keyfind(erlang_md5, 1, CouchConfig) of
         []
 end,
 
-AegisConfig = case lists:keyfind(crypto_module, 1, CouchConfig) of
-    {aegis_key_manager, ""} ->
-        [];
-    {aegis_key_manager, Module} ->
-        [{d, 'AEGIS_KEY_MANAGER', list_to_existing_atom(Module)}];
-    _ ->
-        []
-end,
-
 ProperConfig = case code:lib_dir(proper) of
     {error, bad_name} -> [];
     _ -> [{d, 'WITH_PROPER'}]
@@ -232,7 +223,7 @@ AddConfig = [
         {d, 'COUCHDB_VERSION', Version},
         {d, 'COUCHDB_GIT_SHA', GitSha},
         {i, "../"}
-    ] ++ MD5Config ++ AegisConfig ++ ProperConfig},
+    ] ++ MD5Config ++ ProperConfig},
     {port_env, PortEnvOverrides},
     {eunit_compile_opts, PlatformDefines}
 ].
diff --git a/src/fabric/src/fabric.app.src b/src/fabric/src/fabric.app.src
index 0538b19..a7059fd 100644
--- a/src/fabric/src/fabric.app.src
+++ b/src/fabric/src/fabric.app.src
@@ -28,6 +28,7 @@
         mem3,
         couch_log,
         couch_stats,
-        erlfdb
+        erlfdb,
+        aegis
     ]}
 ]}.
diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index 96f60e6..a78f073 100644
--- a/src/fabric/src/fabric2_fdb.erl
+++ b/src/fabric/src/fabric2_fdb.erl
@@ -236,7 +236,7 @@ create(#{} = Db0, Options) ->
 
         db_options => Options1
     },
-    aegis:create(Db2, Options).
+    aegis:init_db(Db2, Options).
 
 
 open(#{} = Db0, Options) ->
@@ -281,7 +281,7 @@ open(#{} = Db0, Options) ->
     },
 
     Db3 = load_config(Db2),
-    Db4 = aegis:open(Db3, Options),
+    Db4 = aegis:open_db(Db3),
 
     case {UUID, Db4} of
         {undefined, _} -> ok;

Reply via email to