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;