You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2020/04/12 11:41:48 UTC

[couchdb] branch aegis2 updated (39e6766 -> dcc57c7)

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

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


 discard 39e6766  Add encryption for database values
     new dcc57c7  Add encryption for database values

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (39e6766)
            \
             N -- N -- N   refs/heads/aegis2 (dcc57c7)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 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.


Summary of changes:
 src/aegis/src/aegis.erl        | 37 +++++++++++++++++++++++++++++--------
 src/fabric/src/fabric2_fdb.erl |  2 +-
 2 files changed, 30 insertions(+), 9 deletions(-)


[couchdb] 01/01: Add encryption for database values

Posted by rn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit dcc57c7fdbdbdd9b7cc4a15e6355ea9b18573a3e
Author: Robert Newson <rn...@apache.org>
AuthorDate: Wed Apr 8 16:40:26 2020 +0100

    Add encryption for database values
---
 configure                                          |  19 +++
 rebar.config.script                                |   1 +
 rel/reltool.config                                 |   1 +
 src/aegis/src/aegis.app.src                        |  31 ++++
 src/aegis/src/aegis.erl                            | 166 +++++++++++++++++++++
 src/aegis/src/aegis.hrl                            |  57 +++++++
 .../src/aegis_keywrap.erl}                         |  34 ++---
 src/aegis/test/aegis_basic_test.erl                |  17 +++
 src/couch/rebar.config.script                      |  11 +-
 src/couch_views/src/couch_views_fdb.erl            |   8 +-
 src/fabric/include/fabric2.hrl                     |   1 +
 src/fabric/src/fabric2_fdb.erl                     |  31 ++--
 12 files changed, 339 insertions(+), 38 deletions(-)

diff --git a/configure b/configure
index 38e62e3..5bd40d3 100755
--- a/configure
+++ b/configure
@@ -96,6 +96,24 @@ parse_opts() {
                 continue
                 ;;
 
+            --key-manager)
+                if [ -n "$2" ]; then
+                    eval AEGIS_KEY_MANAGER=$2
+                    shift 2
+                    continue
+                else
+                    printf 'ERROR: "--key-manager" requires a non-empty argument.\n' >&2
+                    exit 1
+                fi
+                ;;
+            --key-manager=?*)
+                eval AEGIS_KEY_MANAGER=${1#*=}
+                ;;
+            --key-manager=)
+                printf 'ERROR: "--key-manager" requires a non-empty argument.\n' >&2
+                exit 1
+                ;;
+
             --dev)
                 WITH_DOCS=0
                 WITH_FAUXTON=0
@@ -241,6 +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"},
 {spidermonkey_version, "$SM_VSN"}.
 EOF
 
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..51b608d
--- /dev/null
+++ b/src/aegis/src/aegis.app.src
@@ -0,0 +1,31 @@
+% 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},
+  {applications,
+   [kernel,
+    stdlib,
+    crypto,
+    couch_log,
+    base64,
+    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..fd23624
--- /dev/null
+++ b/src/aegis/src/aegis.erl
@@ -0,0 +1,166 @@
+% 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).
+-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,
+
+    decrypt/2,
+    decrypt/3,
+    encrypt/3,
+    wrap_fold_fun/2
+]).
+
+create(#{} = Db, Options) ->
+    #{
+        name := Name,
+        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),
+
+    couch_log:debug("aegis action=create name=~s hash=~s wrapped=~s prefix=~s",
+        [Name, hash(DbKey), hex(WrappedKey), hex(DbPrefix)]),
+
+    Db#{
+        aegis => DbKey
+    }.
+
+
+open(#{} = Db, Options) ->
+    #{
+        name := Name,
+        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),
+
+    couch_log:debug("aegis action=open name=~s hash=~s  wrapped=~s prefix=~s",
+        [Name, hash(DbKey), hex(WrappedKey), hex(DbPrefix)]),
+
+    Db#{
+        aegis => DbKey
+    }.
+
+
+encrypt(#{} = _Db, Key, <<>>) when is_binary(Key) ->
+    <<>>;
+
+encrypt(#{} = Db, Key, Value)  when is_binary(Key), is_binary(Value) ->
+    #{
+        name := Name,
+        aegis := DbKey,
+        uuid := UUID
+    } = Db,
+
+    IV = crypto:strong_rand_bytes(12),
+
+    {CipherText, <<CipherTag:128>>} =
+        ?aes_gcm_encrypt(
+           derive(DbKey, <<UUID/binary, 0:8, Key/binary>>),
+           IV,
+           <<>>,
+           Value),
+    Result = <<1:8, IV:12/binary, CipherTag:128, CipherText/binary>>,
+
+    couch_log:debug("aegis action=encrypt name=~s hash=~s key=~s inflation=~.2f%",
+        [Name, hash(DbKey), hex(Key),
+        byte_size(Result) / byte_size(Value) * 100]),
+
+    Result.
+
+
+
+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(#{} = _Db, Key, <<>>) when is_binary(Key) ->
+    <<>>;
+
+decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
+    #{
+        name := Name,
+        aegis := DbKey,
+        uuid := UUID
+    } = Db,
+
+    couch_log:debug("aegis action=decrypt name=~s hash=~s key=~s",
+        [Name, hash(DbKey), hex(Key)]),
+
+    case Value of
+        <<1:8, IV:12/binary, CipherTag:128, CipherText/binary>> ->
+            Decrypted =
+            ?aes_gcm_decrypt(
+                derive(DbKey, <<UUID/binary, 0:8, Key/binary>>),
+                IV,
+                <<>>,
+                CipherText,
+                <<CipherTag:128>>),
+            if Decrypted /= error -> Decrypted; true ->
+                erlang:error(decryption_failed)
+            end;
+        _ ->
+            erlang:error(not_ciphertext)
+    end.
+
+
+wrap_fold_fun(Db, Fun) when is_function(Fun, 2) ->
+    fun({Key, Value}, Acc) ->
+        Fun(decrypt(Db, {Key, Value}), Acc)
+    end.
+
+
+%% Private functions
+
+derive(KEK, KeyMaterial) when bit_size(KEK) == 256 ->
+    PlainText = <<1:16, "aegis", 0:8, KeyMaterial/binary, 256:16>>,
+    <<_:256>> = ?sha256_hmac(KEK, PlainText).
+
+
+hex(Data) ->
+    couch_util:to_hex(Data).
+
+
+hash(Data) ->
+    hex(crypto:hash(sha256, Data)).
diff --git a/src/aegis/src/aegis.hrl b/src/aegis/src/aegis.hrl
new file mode 100644
index 0000000..2a2a2dc
--- /dev/null
+++ b/src/aegis/src/aegis.hrl
@@ -0,0 +1,57 @@
+% 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.
+
+%% Assume old crypto api
+
+-define(sha256_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})).
+
+-define(aes_ecb_encrypt(Key, Data),
+	crypto:block_encrypt(aes_ecb, Key, Data)).
+
+-define(aes_ecb_decrypt(Key, Data),
+	crypto:block_decrypt(aes_ecb, Key, Data)).
+
+%% Replace macros if new crypto api is available
+-ifdef(OTP_RELEASE).
+-if(?OTP_RELEASE >= 22).
+
+-undef(sha256_hmac).
+-define(sha256_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)).
+
+-define(key_alg(Key), case bit_size(Key) of
+	128 -> aes_128_ecb; 192 -> aes_192_ecb; 256 -> aes_256_ecb end).
+
+-undef(aes_ecb_encrypt).
+-define(aes_ecb_encrypt(Key, Data),
+        crypto:crypto_one_time(?key_alg(Key), Key, Data, true)).
+
+-undef(aes_ecb_decrypt).
+-define(aes_ecb_decrypt(Key, Data),
+        crypto:crypto_one_time(?key_alg(Key), Key, Data, false)).
+
+-endif.
+-endif.
\ No newline at end of file
diff --git a/src/couch/src/couch_keywrap.erl b/src/aegis/src/aegis_keywrap.erl
similarity index 84%
rename from src/couch/src/couch_keywrap.erl
rename to src/aegis/src/aegis_keywrap.erl
index 0d1e3f5..58c7668 100644
--- a/src/couch/src/couch_keywrap.erl
+++ b/src/aegis/src/aegis_keywrap.erl
@@ -1,4 +1,17 @@
--module(couch_keywrap).
+% 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_keywrap).
+-include("aegis.hrl").
 
 %% Implementation of NIST Special Publication 800-38F
 %% For wrapping and unwrapping keys with AES.
@@ -7,25 +20,6 @@
 
 -define(ICV1, 16#A6A6A6A6A6A6A6A6).
 
-%% Assume old crypto api
--define(aes_ecb_encrypt(Key, Data),
-        crypto:block_encrypt(aes_ecb, Key, Data)).
--define(aes_ecb_decrypt(Key, Data),
-        crypto:block_decrypt(aes_ecb, Key, Data)).
-
-%% Replace macros if new crypto api is available
--ifdef(OTP_RELEASE).
--if(?OTP_RELEASE >= 22).
--define(key_alg(Key), case bit_size(Key) of 128 -> aes_128_ecb; 192 -> aes_192_ecb; 256 -> aes_256_ecb end).
--undef(aes_ecb_encrypt).
--define(aes_ecb_encrypt(Key, Data),
-        crypto:crypto_one_time(?key_alg(Key), Key, Data, true)).
--undef(aes_ecb_decrypt).
--define(aes_ecb_decrypt(Key, Data),
-        crypto:crypto_one_time(?key_alg(Key), Key, Data, false)).
--endif.
--endif.
-
 -spec key_wrap(WrappingKey :: binary(), KeyToWrap :: binary()) -> binary().
 key_wrap(WrappingKey, KeyToWrap)
   when is_binary(WrappingKey), bit_size(KeyToWrap) rem 64 == 0 ->
diff --git a/src/aegis/test/aegis_basic_test.erl b/src/aegis/test/aegis_basic_test.erl
new file mode 100644
index 0000000..61d9737
--- /dev/null
+++ b/src/aegis/test/aegis_basic_test.erl
@@ -0,0 +1,17 @@
+% 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">>}).
diff --git a/src/couch/rebar.config.script b/src/couch/rebar.config.script
index 91e24d9..e281eab 100644
--- a/src/couch/rebar.config.script
+++ b/src/couch/rebar.config.script
@@ -92,6 +92,15 @@ 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'}]
@@ -223,7 +232,7 @@ AddConfig = [
         {d, 'COUCHDB_VERSION', Version},
         {d, 'COUCHDB_GIT_SHA', GitSha},
         {i, "../"}
-    ] ++ MD5Config ++ ProperConfig},
+    ] ++ MD5Config ++ AegisConfig ++ ProperConfig},
     {port_env, PortEnvOverrides},
     {eunit_compile_opts, PlatformDefines}
 ].
diff --git a/src/couch_views/src/couch_views_fdb.erl b/src/couch_views/src/couch_views_fdb.erl
index 3b008d4..dacfdf9 100644
--- a/src/couch_views/src/couch_views_fdb.erl
+++ b/src/couch_views/src/couch_views_fdb.erl
@@ -158,7 +158,7 @@ fold_map_idx(TxDb, Sig, ViewId, Options, Callback, Acc0) ->
         callback => Callback,
         acc => Acc0
         },
-    Fun = fun fold_fwd/2,
+    Fun = aegis:wrap_fold_fun(TxDb, fun fold_fwd/2),
 
     #{
         acc := Acc1
@@ -283,7 +283,7 @@ update_id_idx(TxDb, Sig, ViewId, DocId, NewRows, KVSize) ->
 
     Key = id_idx_key(DbPrefix, Sig, DocId, ViewId),
     Val = couch_views_encoding:encode([length(NewRows), KVSize, Unique]),
-    ok = erlfdb:set(Tx, Key, Val).
+    ok = erlfdb:set(Tx, Key, aegis:encrypt(TxDb, Key, Val)).
 
 
 update_map_idx(TxDb, Sig, ViewId, DocId, ExistingKeys, NewRows) ->
@@ -303,7 +303,7 @@ update_map_idx(TxDb, Sig, ViewId, DocId, ExistingKeys, NewRows) ->
     lists:foreach(fun({DupeId, Key1, Key2, EV}) ->
         KK = map_idx_key(MapIdxPrefix, {Key1, DocId}, DupeId),
         Val = erlfdb_tuple:pack({Key2, EV}),
-        ok = erlfdb:set(Tx, KK, Val)
+        ok = erlfdb:set(Tx, KK, aegis:encrypt(TxDb, KK, Val))
     end, KVsToAdd).
 
 
@@ -318,7 +318,7 @@ get_view_keys(TxDb, Sig, DocId) ->
                 erlfdb_tuple:unpack(K, DbPrefix),
         [TotalKeys, TotalSize, UniqueKeys] = couch_views_encoding:decode(V),
         {ViewId, TotalKeys, TotalSize, UniqueKeys}
-    end, erlfdb:get_range(Tx, Start, End, [])).
+    end, aegis:decrypt(TxDb, erlfdb:get_range(Tx, Start, End, []))).
 
 
 update_row_count(TxDb, Sig, ViewId, Increment) ->
diff --git a/src/fabric/include/fabric2.hrl b/src/fabric/include/fabric2.hrl
index 0c07575..b4fe4f7 100644
--- a/src/fabric/include/fabric2.hrl
+++ b/src/fabric/include/fabric2.hrl
@@ -40,6 +40,7 @@
 -define(DB_LOCAL_DOC_BODIES, 25).
 -define(DB_ATT_NAMES, 26).
 -define(DB_SEARCH, 27).
+-define(DB_AEGIS, 28).
 
 
 % Versions
diff --git a/src/fabric/src/fabric2_fdb.erl b/src/fabric/src/fabric2_fdb.erl
index 2295a56..96f60e6 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, Options).
 
 
 open(#{} = Db0, Options) ->
@@ -280,14 +281,15 @@ open(#{} = Db0, Options) ->
     },
 
     Db3 = load_config(Db2),
+    Db4 = aegis:open(Db3, Options),
 
-    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
@@ -630,9 +632,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).
@@ -649,7 +652,7 @@ get_local_doc(#{} = Db0, <<?LOCAL_DOC_PREFIX, _/binary>> = DocId) ->
 
     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).
 
@@ -878,7 +881,9 @@ write_local_doc(#{} = Db0, Doc) ->
             % 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 +911,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.
 
 
@@ -925,7 +930,7 @@ write_attachment(#{} = Db, DocId, Data) when is_binary(Data) ->
 
     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}.
@@ -1193,7 +1198,7 @@ write_doc_body(#{} = Db0, #doc{} = Doc) ->
 
     Rows = doc_to_fdb(Db, Doc),
     lists:foreach(fun({Key, Value}) ->
-        ok = erlfdb:set(Tx, Key, Value)
+        ok = erlfdb:set(Tx, Key, aegis:encrypt(Db, Key, Value))
     end, Rows).