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 2022/05/10 13:34:02 UTC

[couchdb] 01/01: Add key rotation facility

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

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

commit 4973c0494e8d83fea5ab8bca8b3e23e0daa6c6fb
Author: Robert Newson <rn...@apache.org>
AuthorDate: Tue May 10 08:29:45 2022 +0100

    Add key rotation facility
---
 rel/overlay/etc/default.ini  |   9 +++-
 src/couch/src/couch_file.erl | 107 ++++++++++++++++++++++++++-----------------
 2 files changed, 72 insertions(+), 44 deletions(-)

diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini
index 98349f5eb..bfda46f44 100644
--- a/rel/overlay/etc/default.ini
+++ b/rel/overlay/etc/default.ini
@@ -6,8 +6,6 @@ name = {{package_author_name}}
 uuid = {{uuid}}
 database_dir = {{data_dir}}
 view_index_dir = {{view_index_dir}}
-encryption_password = super_secret_password
-encryption_salt = no_saltier_than_this
 ; util_driver_dir =
 ; plugin_dir =
 ;os_process_timeout = 5000 ; 5 seconds. for view servers.
@@ -749,3 +747,10 @@ port = {{prometheus_port}}
 ; to disable this setting could be if the views need an upgrade but located on
 ; read-only file system.
 ;commit_on_header_upgrade = true
+
+[encryption]
+wrapping_key_id = bec46c7438e685d6
+
+[encryption_keys]
+254abda45788029b = 0585d781ef090ee26f329a36f66935ce
+bec46c7438e685d6 = e6f598615eb7502b473b101d64f56410
\ No newline at end of file
diff --git a/src/couch/src/couch_file.erl b/src/couch/src/couch_file.erl
index 6198d8f9a..d63ebde7b 100644
--- a/src/couch/src/couch_file.erl
+++ b/src/couch/src/couch_file.erl
@@ -23,7 +23,6 @@
 -define(IS_OLD_STATE(S), is_pid(S#file.db_monitor)).
 -define(PREFIX_SIZE, 5).
 -define(DEFAULT_READ_COUNT, 1024).
--define(ENCRYPTED_HEADER, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).
 
 -type block_id() :: non_neg_integer().
 -type location() :: non_neg_integer().
@@ -39,6 +38,12 @@
     dec
 }).
 
+-record(encryption_header, {
+    version = 1,
+    wrapping_key_id,
+    wrapped_key
+}).
+
 % public API
 -export([open/1, open/2, close/1, bytes/1, sync/1, truncate/2, set_db_pid/2]).
 -export([pread_term/2, pread_iolist/2, pread_binary/2]).
@@ -619,18 +624,10 @@ handle_call({append_bins, Bins}, _From, #file{eof = Pos} = File) ->
         Error ->
             {reply, Error, reset_eof(File)}
     end;
-handle_call({write_header, Bin}, _From, #file{eof = Pos} = File) ->
-    BinSize = byte_size(Bin),
-    case Pos rem ?SIZE_BLOCK of
-        0 ->
-            Padding = <<>>;
-        BlockOffset ->
-            Padding = <<0:(8 * (?SIZE_BLOCK - BlockOffset))>>
-    end,
-    FinalBin = [Padding, <<1, BinSize:32/integer>> | make_blocks(5, [Bin])],
-    case encrypted_write(File, FinalBin) of
-        ok ->
-            {reply, ok, File#file{eof = Pos + iolist_size(FinalBin)}};
+handle_call({write_header, Bin}, _From, #file{} = File) ->
+    case write_header_int(File, Bin) of
+        {ok, File1} ->
+            {reply, ok, File1};
         Error ->
             {reply, Error, reset_eof(File)}
     end;
@@ -700,6 +697,22 @@ load_header(#file{} = File, Pos, HeaderLen, RestBlock) ->
     Md5Sig = couch_hash:md5_hash(HeaderBin),
     {ok, HeaderBin}.
 
+write_header_int(#file{eof = Pos} = File, Bin) ->
+    BinSize = byte_size(Bin),
+    case Pos rem ?SIZE_BLOCK of
+        0 ->
+            Padding = <<>>;
+        BlockOffset ->
+            Padding = <<0:(8 * (?SIZE_BLOCK - BlockOffset))>>
+    end,
+    FinalBin = [Padding, <<1, BinSize:32/integer>> | make_blocks(5, [Bin])],
+    case encrypted_write(File, FinalBin) of
+        ok ->
+            {ok, File#file{eof = Pos + iolist_size(FinalBin)}};
+        Else ->
+            Else
+    end.
+
 %% Read multiple block locations using a single file:pread/2.
 -spec find_header(file:fd(), block_id(), non_neg_integer()) ->
     {ok, binary()} | no_valid_header.
@@ -928,29 +941,43 @@ reset_eof(#file{} = File) ->
     File#file{eof = Eof}.
 
 
-%% we've wiped all the data, including the wrapped key, so we need a new one.
-init_crypto(#file{eof = 0} = File) ->
+%% new file or we've wiped all the data, including the wrapped key, so we need a new one.
+init_crypto(#file{eof = 0} = File0) ->
     Key = crypto:strong_rand_bytes(32),
-    WrappedKey = aegis:wrap_key(master_key(), [], Key),
-    Header = <<?ENCRYPTED_HEADER, WrappedKey/binary>>,
-    ok = file:write(File#file.fd, Header),
-    ok = file:sync(File#file.fd),
-    {ok, init_crypto(File#file{eof = iolist_size(Header)}, Key)};
+    {WrappingKeyId, WrappingKey} = current_wrapping_key(),
+    WrappedKey = aegis:wrap_key(WrappingKey, [WrappingKeyId], Key),
+    Header = #encryption_header{
+        wrapping_key_id = WrappingKeyId,
+        wrapped_key = WrappedKey
+    },
+    HeaderBin = term_to_binary(Header),
+    Md5 = couch_hash:md5_hash(HeaderBin),
+    FinalBin = <<Md5/binary, HeaderBin/binary>>,
+    case write_header_int(File0#file{enc = unencrypted}, FinalBin) of
+        {ok, File1} ->
+            ok = file:sync(File1#file.fd),
+            {ok, init_crypto(File1, Key)};
+        Else ->
+            Else
+    end;
 
 %% we're opening an existing file and need to unwrap the key.
 init_crypto(#file{enc = undefined, dec = undefined} = File) ->
-    case file:pread(File#file.fd, 0, 64) of
-        {ok, <<?ENCRYPTED_HEADER, WrappedKey:48/binary>>} ->
-            case aegis:unwrap_key(master_key(), [], WrappedKey) of
+    {ok, HeaderBin} = load_header(File#file{dec = unencrypted}, 0),
+    case binary_to_term(HeaderBin, [safe]) of
+        #encryption_header{
+           version = 1,
+           wrapping_key_id = WrappingKeyId,
+           wrapped_key = WrappedKey
+          } ->
+            case aegis:unwrap_key(wrapping_key(WrappingKeyId), [WrappingKeyId], WrappedKey) of
                 fail ->
                     {error, unwrap_failed};
                 Key when is_binary(Key) ->
                     {ok, init_crypto(File, Key)}
             end;
-        {ok, _} ->
-            {ok, File#file{enc = unencrypted, dec = unencrypted}};
-        Else ->
-            Else
+        _ ->
+            {ok, File#file{enc = unencrypted, dec = unencrypted}}
     end;
 
 %% we're opening an existing file that contains a wrapped key
@@ -1027,27 +1054,23 @@ unpad(Pos, Bin) when is_binary(Bin) ->
     Result.
 
 
-master_key() ->
-    couch_pbkdf2:pbkdf2(sha256, master_password(), master_salt(), 100000).
+current_wrapping_key() ->
+    KeyId = config:get("encryption", "wrapping_key_id"),
+    {?l2b(KeyId), wrapping_key(KeyId)}.
 
 
-master_password() ->
-    case config:get("couchdb", "encryption_password") of
-        undefined ->
-            undefined;
-        Password ->
-            ?l2b(Password)
-    end.
+wrapping_key(KeyId) ->
+    get_config_binary("encryption_keys", couch_util:to_list(KeyId), undefined).
 
-master_salt() ->
-    case config:get("couchdb", "encryption_salt") of
+
+get_config_binary(Section, Key, Default) ->
+    case config:get(Section, Key) of
         undefined ->
-            undefined;
-        Salt ->
-            ?l2b(Salt)
+            Default;
+        Value ->
+            ?l2b(Value)
     end.
 
-
 -ifdef(TEST).
 -include_lib("couch/include/couch_eunit.hrl").