diff options
| author | Karl Nilsson <kjnilsson@gmail.com> | 2016-10-12 14:44:36 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-10-12 14:44:36 +0100 |
| commit | ae67c3e121322f70a10359675786adb4df7baddb (patch) | |
| tree | 871fffe343cf1e9c9a2eebc70f6acc38ef610de4 | |
| parent | 157cdaa0d0c9b7753fbfcb2dba37cd48b8101407 (diff) | |
| parent | c5991c696f7a5751af1899729ff5a15b1f58950e (diff) | |
| download | rabbitmq-server-git-ae67c3e121322f70a10359675786adb4df7baddb.tar.gz | |
Merge pull request #994 from rabbitmq/rabbitmq-server-979
Add optional password based encryption of configuration values
| -rw-r--r-- | include/rabbit_cli.hrl | 13 | ||||
| -rw-r--r-- | src/rabbit.app.src | 6 | ||||
| -rw-r--r-- | src/rabbit.erl | 106 | ||||
| -rw-r--r-- | src/rabbit_control_main.erl | 16 | ||||
| -rw-r--r-- | src/rabbit_control_pbe.erl | 79 | ||||
| -rw-r--r-- | src/rabbit_pbe.erl | 194 | ||||
| -rw-r--r-- | test/unit_SUITE.erl | 279 | ||||
| -rw-r--r-- | test/unit_SUITE_data/lib/rabbit_shovel_test/ebin/rabbit_shovel_test.app | 46 | ||||
| -rw-r--r-- | test/unit_SUITE_data/rabbit_shovel_test.passphrase | 1 |
9 files changed, 736 insertions, 4 deletions
diff --git a/include/rabbit_cli.hrl b/include/rabbit_cli.hrl index b1cf41261f..53be9fcda0 100644 --- a/include/rabbit_cli.hrl +++ b/include/rabbit_cli.hrl @@ -31,6 +31,12 @@ -define(ONLINE_OPT, "--online"). -define(LOCAL_OPT, "--local"). +-define(DECODE_OPT, "--decode"). +-define(CIPHER_OPT, "--cipher"). +-define(HASH_OPT, "--hash"). +-define(ITERATIONS_OPT, "--iterations"). +-define(LIST_CIPHERS_OPT, "--list-ciphers"). +-define(LIST_HASHES_OPT, "--list-hashes"). -define(NODE_DEF(Node), {?NODE_OPT, {option, Node}}). -define(QUIET_DEF, {?QUIET_OPT, flag}). @@ -48,6 +54,13 @@ -define(OFFLINE_DEF, {?OFFLINE_OPT, flag}). -define(ONLINE_DEF, {?ONLINE_OPT, flag}). -define(LOCAL_DEF, {?LOCAL_OPT, flag}). +-define(DECODE_DEF, {?DECODE_OPT, flag}). +-define(CIPHER_DEF, {?CIPHER_OPT, {option, atom_to_list(rabbit_pbe:default_cipher())}}). +-define(HASH_DEF, {?HASH_OPT, {option, atom_to_list(rabbit_pbe:default_hash())}}). +-define(ITERATIONS_DEF, {?ITERATIONS_OPT, {option, integer_to_list(rabbit_pbe:default_iterations())}}). +-define(LIST_CIPHERS_DEF, {?LIST_CIPHERS_OPT, flag}). +-define(LIST_HASHES_DEF, {?LIST_HASHES_OPT, flag}). + %% Subset of standartized exit codes from sysexits.h, see %% https://github.com/rabbitmq/rabbitmq-server/issues/396 for discussion. diff --git a/src/rabbit.app.src b/src/rabbit.app.src index 217aad593e..883b71cb77 100644 --- a/src/rabbit.app.src +++ b/src/rabbit.app.src @@ -100,6 +100,12 @@ %% see rabbitmq-server#248 %% and rabbitmq-server#667 {channel_operation_timeout, 15000}, + {decoder_config, [ + {cipher, aes_cbc256}, + {hash, sha512}, + {iterations, 1000}, + {passphrase, undefined} + ]}, %% rabbitmq-server-973 {lazy_queue_explicit_gc_run_operation_threshold, 250} ]}]}. diff --git a/src/rabbit.erl b/src/rabbit.erl index a86fd97925..9624aca184 100644 --- a/src/rabbit.erl +++ b/src/rabbit.erl @@ -24,7 +24,7 @@ start_fhc/0]). -export([start/2, stop/1, prep_stop/1]). -export([start_apps/1, stop_apps/1]). --export([log_location/1, config_files/0]). %% for testing and mgmt-agent +-export([log_location/1, config_files/0, decrypt_config/2]). %% for testing and mgmt-agent %%--------------------------------------------------------------------------- %% Boot steps. @@ -442,6 +442,38 @@ stop_and_halt() -> start_apps(Apps) -> app_utils:load_applications(Apps), + + DecoderConfig = case application:get_env(rabbit, decoder_config) of + undefined -> + []; + {ok, Val} -> + Val + end, + PassPhrase = case proplists:get_value(passphrase, DecoderConfig) of + prompt -> + IoDevice = get_input_iodevice(), + io:setopts(IoDevice, [{echo, false}]), + PP = lists:droplast(io:get_line(IoDevice, + "\nPlease enter the passphrase to unlock encrypted " + "configuration entries.\n\nPassphrase: ")), + io:setopts(IoDevice, [{echo, true}]), + io:format(IoDevice, "~n", []), + PP; + {file, Filename} -> + {ok, File} = file:read_file(Filename), + [PP|_] = binary:split(File, [<<"\r\n">>, <<"\n">>]), + PP; + PP -> + PP + end, + Algo = { + proplists:get_value(cipher, DecoderConfig, rabbit_pbe:default_cipher()), + proplists:get_value(hash, DecoderConfig, rabbit_pbe:default_hash()), + proplists:get_value(iterations, DecoderConfig, rabbit_pbe:default_iterations()), + PassPhrase + }, + decrypt_config(Apps, Algo), + OrderedApps = app_utils:app_dependency_order(Apps, false), case lists:member(rabbit, Apps) of false -> rabbit_boot_steps:run_boot_steps(Apps); %% plugin activation @@ -450,6 +482,78 @@ start_apps(Apps) -> ok = app_utils:start_applications(OrderedApps, handle_app_error(could_not_start)). +%% This function retrieves the correct IoDevice for requesting +%% input. The problem with using the default IoDevice is that +%% the Erlang shell prevents us from getting the input. +%% +%% Instead we therefore look for the io process used by the +%% shell and if it can't be found (because the shell is not +%% started e.g with -noshell) we use the 'user' process. +%% +%% This function will not work when either -oldshell or -noinput +%% options are passed to erl. +get_input_iodevice() -> + case whereis(user) of + undefined -> user; + User -> + case group:interfaces(User) of + [] -> + user; + [{user_drv, Drv}] -> + case user_drv:interfaces(Drv) of + [] -> + user; + [{current_group, IoDevice}] -> + IoDevice + end + end + end. + +decrypt_config([], _) -> + ok; +decrypt_config([App|Apps], Algo) -> + decrypt_app(App, application:get_all_env(App), Algo), + decrypt_config(Apps, Algo). + +decrypt_app(_, [], _) -> + ok; +decrypt_app(App, [{Key, Value}|Tail], Algo) -> + try begin + case decrypt(Value, Algo) of + Value -> + ok; + NewValue -> + application:set_env(App, Key, NewValue) + end + end + catch + exit:{bad_configuration, decoder_config} -> + exit({bad_configuration, decoder_config}); + _:Msg -> + rabbit_log:info("Error while decrypting key '~p'. Please check encrypted value, passphrase, and encryption configuration~n", [Key]), + exit({decryption_error, {key, Key}, Msg}) + end, + decrypt_app(App, Tail, Algo). + +decrypt({encrypted, _}, {_, _, _, undefined}) -> + exit({bad_configuration, decoder_config}); +decrypt({encrypted, EncValue}, {Cipher, Hash, Iterations, Password}) -> + rabbit_pbe:decrypt_term(Cipher, Hash, Iterations, Password, EncValue); +decrypt(List, Algo) when is_list(List) -> + decrypt_list(List, Algo, []); +decrypt(Value, _) -> + Value. + +%% We make no distinction between strings and other lists. +%% When we receive a string, we loop through each element +%% and ultimately return the string unmodified, as intended. +decrypt_list([], _, Acc) -> + lists:reverse(Acc); +decrypt_list([{Key, Value}|Tail], Algo, Acc) when Key =/= encrypted -> + decrypt_list(Tail, Algo, [{Key, decrypt(Value, Algo)}|Acc]); +decrypt_list([Value|Tail], Algo, Acc) -> + decrypt_list(Tail, Algo, [decrypt(Value, Algo)|Acc]). + stop_apps(Apps) -> ok = app_utils:stop_applications( Apps, handle_app_error(error_during_shutdown)), diff --git a/src/rabbit_control_main.erl b/src/rabbit_control_main.erl index 92898c2a2c..8c245892b7 100644 --- a/src/rabbit_control_main.erl +++ b/src/rabbit_control_main.erl @@ -92,7 +92,8 @@ {trace_off, [?VHOST_DEF]}, set_vm_memory_high_watermark, set_disk_free_limit, - help + help, + {encode, [?DECODE_DEF, ?CIPHER_DEF, ?HASH_DEF, ?ITERATIONS_DEF, ?LIST_CIPHERS_DEF, ?LIST_HASHES_DEF]} ]). -define(GLOBAL_QUERIES, @@ -114,7 +115,7 @@ [stop, stop_app, start_app, wait, reset, force_reset, rotate_logs, join_cluster, change_cluster_node_type, update_cluster_nodes, forget_cluster_node, rename_cluster_node, cluster_status, status, - environment, eval, force_boot, help, hipe_compile]). + environment, eval, force_boot, help, hipe_compile, encode]). %% [Command | {Command, DefaultTimeoutInMilliSeconds}] -define(COMMANDS_WITH_TIMEOUT, @@ -579,6 +580,17 @@ action(eval, Node, [Expr], _Opts, _Inform) -> action(help, _Node, _Args, _Opts, _Inform) -> io:format("~s", [rabbit_ctl_usage:usage()]); +action(encode, _Node, Args, Opts, _Inform) -> + ListCiphers = lists:member({?LIST_CIPHERS_OPT, true}, Opts), + ListHashes = lists:member({?LIST_HASHES_OPT, true}, Opts), + Decode = lists:member({?DECODE_OPT, true}, Opts), + Cipher = list_to_atom(proplists:get_value(?CIPHER_OPT, Opts)), + Hash = list_to_atom(proplists:get_value(?HASH_OPT, Opts)), + Iterations = list_to_integer(proplists:get_value(?ITERATIONS_OPT, Opts)), + + {_, Msg} = rabbit_control_pbe:encode(ListCiphers, ListHashes, Decode, Cipher, Hash, Iterations, Args), + io:format(Msg ++ "~n"); + action(Command, Node, Args, Opts, Inform) -> %% For backward compatibility, run commands accepting a timeout with %% the default timeout. diff --git a/src/rabbit_control_pbe.erl b/src/rabbit_control_pbe.erl new file mode 100644 index 0000000000..2fa2c90a6e --- /dev/null +++ b/src/rabbit_control_pbe.erl @@ -0,0 +1,79 @@ +% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (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.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2007-2016 Pivotal Software, Inc. All rights reserved. +%% + +-module(rabbit_control_pbe). + +-export([encode/7]). + +% for testing purposes +-export([evaluate_input_as_term/1]). + +encode(ListCiphers, _ListHashes, _Decode, _Cipher, _Hash, _Iterations, _Args) when ListCiphers -> + {ok, io_lib:format("~p", [rabbit_pbe:supported_ciphers()])}; + +encode(_ListCiphers, ListHashes, _Decode, _Cipher, _Hash, _Iterations, _Args) when ListHashes -> + {ok, io_lib:format("~p", [rabbit_pbe:supported_hashes()])}; + +encode(_ListCiphers, _ListHashes, Decode, Cipher, Hash, Iterations, Args) -> + CipherExists = lists:member(Cipher, rabbit_pbe:supported_ciphers()), + HashExists = lists:member(Hash, rabbit_pbe:supported_hashes()), + encode_encrypt_decrypt(CipherExists, HashExists, Decode, Cipher, Hash, Iterations, Args). + +encode_encrypt_decrypt(CipherExists, _HashExists, _Decode, _Cipher, _Hash, _Iterations, _Args) when CipherExists =:= false -> + {error, io_lib:format("The requested cipher is not supported", [])}; + +encode_encrypt_decrypt(_CipherExists, HashExists, _Decode, _Cipher, _Hash, _Iterations, _Args) when HashExists =:= false -> + {error, io_lib:format("The requested hash is not supported", [])}; + +encode_encrypt_decrypt(_CipherExists, _HashExists, _Decode, _Cipher, _Hash, Iterations, _Args) when Iterations =< 0 -> + {error, io_lib:format("The requested number of iterations is incorrect", [])}; + +encode_encrypt_decrypt(_CipherExists, _HashExists, Decode, Cipher, Hash, Iterations, Args) when length(Args) == 2, Decode =:= false -> + [Value, PassPhrase] = Args, + try begin + TermValue = evaluate_input_as_term(Value), + Result = rabbit_pbe:encrypt_term(Cipher, Hash, Iterations, list_to_binary(PassPhrase), TermValue), + {ok, io_lib:format("~p", [{encrypted, Result}])} + end + catch + _:Msg -> {error, io_lib:format("Error during cipher operation: ~p", [Msg])} + end; + +encode_encrypt_decrypt(_CipherExists, _HashExists, Decode, Cipher, Hash, Iterations, Args) when length(Args) == 2, Decode -> + [Value, PassPhrase] = Args, + try begin + TermValue = evaluate_input_as_term(Value), + TermToDecrypt = case TermValue of + {encrypted, EncryptedTerm} -> + EncryptedTerm; + _ -> + TermValue + end, + Result = rabbit_pbe:decrypt_term(Cipher, Hash, Iterations, list_to_binary(PassPhrase), TermToDecrypt), + {ok, io_lib:format("~p", [Result])} + end + catch + _:Msg -> {error, io_lib:format("Error during cipher operation: ~p", [Msg])} + end; + +encode_encrypt_decrypt(_CipherExists, _HashExists, _Decode, _Cipher, _Hash, _Iterations, _Args) -> + {error, io_lib:format("Please provide a value to encode/decode and a passphrase", [])}. + +evaluate_input_as_term(Input) -> + {ok,Tokens,_EndLine} = erl_scan:string(Input ++ "."), + {ok,AbsForm} = erl_parse:parse_exprs(Tokens), + {value,TermValue,_Bs} = erl_eval:exprs(AbsForm, erl_eval:new_bindings()), + TermValue. diff --git a/src/rabbit_pbe.erl b/src/rabbit_pbe.erl new file mode 100644 index 0000000000..f4998d4a13 --- /dev/null +++ b/src/rabbit_pbe.erl @@ -0,0 +1,194 @@ +%% The contents of this file are subject to the Mozilla Public License +%% Version 1.1 (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.mozilla.org/MPL/ +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and +%% limitations under the License. +%% +%% The Original Code is RabbitMQ. +%% +%% The Initial Developer of the Original Code is GoPivotal, Inc. +%% Copyright (c) 2007-2016 Pivotal Software, Inc. All rights reserved. +%% + +-module(rabbit_pbe). + +-export([supported_ciphers/0, supported_hashes/0, default_cipher/0, default_hash/0, default_iterations/0]). +-export([encrypt_term/5, decrypt_term/5]). +-export([encrypt/5, decrypt/5]). + +%% Supported ciphers and hashes + +supported_ciphers() -> + proplists:get_value(ciphers, crypto:supports()) + -- [aes_ctr, aes_ecb, des_ecb, blowfish_ecb, rc4, aes_gcm]. + +supported_hashes() -> + proplists:get_value(hashs, crypto:supports()) + -- [md4, ripemd160]. + +%% Default encryption parameters (keep those in sync with rabbit.app.src) +default_cipher() -> + aes_cbc256. + +default_hash() -> + sha512. + +default_iterations() -> + 1000. + +%% Encryption/decryption of arbitrary Erlang terms. + +encrypt_term(Cipher, Hash, Iterations, PassPhrase, Term) -> + encrypt(Cipher, Hash, Iterations, PassPhrase, term_to_binary(Term)). + +decrypt_term(Cipher, Hash, Iterations, PassPhrase, Base64Binary) -> + binary_to_term(decrypt(Cipher, Hash, Iterations, PassPhrase, Base64Binary)). + +%% The cipher for encryption is from the list of supported ciphers. +%% The hash for generating the key from the passphrase is from the list +%% of supported hashes. See crypto:supports/0 to obtain both lists. +%% The key is generated by applying the hash N times with N >= 1. +%% +%% The encrypt/5 function returns a base64 binary and the decrypt/5 +%% function accepts that same base64 binary. + +-spec encrypt(crypto:block_cipher(), crypto:hash_algorithms(), + pos_integer(), iodata(), binary()) -> binary(). +encrypt(Cipher, Hash, Iterations, PassPhrase, ClearText) -> + Salt = crypto:strong_rand_bytes(16), + Ivec = crypto:strong_rand_bytes(iv_length(Cipher)), + Key = make_key(Cipher, Hash, Iterations, PassPhrase, Salt), + Binary = crypto:block_encrypt(Cipher, Key, Ivec, pad(Cipher, ClearText)), + base64:encode(<< Salt/binary, Ivec/binary, Binary/binary >>). + +-spec decrypt(crypto:block_cipher(), crypto:hash_algorithms(), + pos_integer(), iodata(), binary()) -> binary(). +decrypt(Cipher, Hash, Iterations, PassPhrase, Base64Binary) -> + IvLength = iv_length(Cipher), + << Salt:16/binary, Ivec:IvLength/binary, Binary/bits >> = base64:decode(Base64Binary), + Key = make_key(Cipher, Hash, Iterations, PassPhrase, Salt), + unpad(crypto:block_decrypt(Cipher, Key, Ivec, Binary)). + +%% Generate a key from a passphrase. + +make_key(Cipher, Hash, Iterations, PassPhrase, Salt) -> + Key = pbdkdf2(PassPhrase, Salt, Iterations, key_length(Cipher), + fun crypto:hmac/4, Hash, hash_length(Hash)), + if + Cipher =:= des3_cbc; Cipher =:= des3_cbf; Cipher =:= des3_cfb; Cipher =:= des_ede3 -> + << A:8/binary, B:8/binary, C:8/binary >> = Key, + [A, B, C]; + true -> + Key + end. + +%% Functions to pad/unpad input to a multiplier of block size. + +pad(Cipher, Data) -> + BlockSize = block_size(Cipher), + N = BlockSize - (byte_size(Data) rem BlockSize), + Pad = list_to_binary(lists:duplicate(N, N)), + <<Data/binary, Pad/binary>>. + +unpad(Data) -> + N = binary:last(Data), + binary:part(Data, 0, byte_size(Data) - N). + +%% These functions are necessary because the current Erlang crypto interface +%% is lacking interfaces to the following OpenSSL functions: +%% +%% * int EVP_MD_size(const EVP_MD *md); +%% * int EVP_CIPHER_iv_length(const EVP_CIPHER *e); +%% * int EVP_CIPHER_key_length(const EVP_CIPHER *e); +%% * int EVP_CIPHER_block_size(const EVP_CIPHER *e); + +hash_length(md4) -> 16; +hash_length(md5) -> 16; +hash_length(sha) -> 20; +hash_length(sha224) -> 28; +hash_length(sha256) -> 32; +hash_length(sha384) -> 48; +hash_length(sha512) -> 64. + +iv_length(des_cbc) -> 8; +iv_length(des_cfb) -> 8; +iv_length(des3_cbc) -> 8; +iv_length(des3_cbf) -> 8; +iv_length(des3_cfb) -> 8; +iv_length(des_ede3) -> 8; +iv_length(blowfish_cbc) -> 8; +iv_length(blowfish_cfb64) -> 8; +iv_length(blowfish_ofb64) -> 8; +iv_length(rc2_cbc) -> 8; +iv_length(aes_cbc) -> 16; +iv_length(aes_cbc128) -> 16; +iv_length(aes_cfb8) -> 16; +iv_length(aes_cfb128) -> 16; +iv_length(aes_cbc256) -> 16; +iv_length(aes_ige256) -> 32. + +key_length(des_cbc) -> 8; +key_length(des_cfb) -> 8; +key_length(des3_cbc) -> 24; +key_length(des3_cbf) -> 24; +key_length(des3_cfb) -> 24; +key_length(des_ede3) -> 24; +key_length(blowfish_cbc) -> 16; +key_length(blowfish_cfb64) -> 16; +key_length(blowfish_ofb64) -> 16; +key_length(rc2_cbc) -> 16; +key_length(aes_cbc) -> 16; +key_length(aes_cbc128) -> 16; +key_length(aes_cfb8) -> 16; +key_length(aes_cfb128) -> 16; +key_length(aes_cbc256) -> 32; +key_length(aes_ige256) -> 16. + +block_size(aes_cbc256) -> 32; +block_size(aes_cbc128) -> 32; +block_size(aes_ige256) -> 32; +block_size(aes_cbc) -> 32; +block_size(_) -> 8. + +%% The following was taken from OTP's lib/public_key/src/pubkey_pbe.erl +%% +%% This is an undocumented interface to password-based encryption algorithms. +%% These functions have been copied here to stay compatible with R16B03. + +%%-------------------------------------------------------------------- +-spec pbdkdf2(string(), iodata(), integer(), integer(), fun(), atom(), integer()) + -> binary(). +%% +%% Description: Implements password based decryption key derive function 2. +%% Exported mainly for testing purposes. +%%-------------------------------------------------------------------- +pbdkdf2(Password, Salt, Count, DerivedKeyLen, Prf, PrfHash, PrfOutputLen)-> + NumBlocks = ceiling(DerivedKeyLen / PrfOutputLen), + NumLastBlockOctets = DerivedKeyLen - (NumBlocks - 1) * PrfOutputLen , + blocks(NumBlocks, NumLastBlockOctets, 1, Password, Salt, + Count, Prf, PrfHash, PrfOutputLen, <<>>). + +blocks(1, N, Index, Password, Salt, Count, Prf, PrfHash, PrfLen, Acc) -> + <<XorSum:N/binary, _/binary>> = xor_sum(Password, Salt, Count, Index, Prf, PrfHash, PrfLen), + <<Acc/binary, XorSum/binary>>; +blocks(NumBlocks, N, Index, Password, Salt, Count, Prf, PrfHash, PrfLen, Acc) -> + XorSum = xor_sum(Password, Salt, Count, Index, Prf, PrfHash, PrfLen), + blocks(NumBlocks -1, N, Index +1, Password, Salt, Count, Prf, PrfHash, + PrfLen, <<Acc/binary, XorSum/binary>>). + +xor_sum(Password, Salt, Count, Index, Prf, PrfHash, PrfLen) -> + Result = Prf(PrfHash, Password, [Salt,<<Index:32/unsigned-big-integer>>], PrfLen), + do_xor_sum(Prf, PrfHash, PrfLen, Result, Password, Count-1, Result). + +do_xor_sum(_, _, _, _, _, 0, Acc) -> + Acc; +do_xor_sum(Prf, PrfHash, PrfLen, Prev, Password, Count, Acc) -> + Result = Prf(PrfHash, Password, Prev, PrfLen), + do_xor_sum(Prf, PrfHash, PrfLen, Result, Password, Count-1, crypto:exor(Acc, Result)). + +ceiling(Float) -> + erlang:round(Float + 0.5). diff --git a/test/unit_SUITE.erl b/test/unit_SUITE.erl index 43e812fa3d..e79dfc46ad 100644 --- a/test/unit_SUITE.erl +++ b/test/unit_SUITE.erl @@ -24,7 +24,8 @@ all() -> [ - {group, parallel_tests} + {group, parallel_tests}, + {group, sequential_tests} ]. groups() -> @@ -41,6 +42,10 @@ groups() -> ]}, content_framing, content_transcoding, + encrypt_decrypt, + encrypt_decrypt_term, + decrypt_config, + rabbitmqctl_encode, pg_local, pmerge, plmerge, @@ -63,12 +68,36 @@ groups() -> {vm_memory_monitor, [parallel], [ parse_line_linux ]} + ]}, + {sequential_tests, [], [ + decrypt_start_app, + decrypt_start_app_file, + decrypt_start_app_undefined, + decrypt_start_app_wrong_passphrase ]} ]. init_per_group(_, Config) -> Config. end_per_group(_, Config) -> Config. +init_per_testcase(TC, Config) when TC =:= decrypt_start_app; + TC =:= decrypt_start_app_file; + TC =:= decrypt_start_app_undefined -> + application:load(rabbit), + Config; +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(TC, _Config) when TC =:= decrypt_start_app; + TC =:= decrypt_start_app_file; + TC =:= decrypt_start_app_undefined -> + application:unload(rabbit), + application:unload(rabbit_shovel_test); +end_per_testcase(decrypt_config, _Config) -> + application:unload(rabbit); +end_per_testcase(_TC, _Config) -> + ok. + %% ------------------------------------------------------------------- %% Argument parsing. %% ------------------------------------------------------------------- @@ -233,6 +262,254 @@ prepend_check(HeaderKey, HeaderTable, Headers) -> rabbit_misc:table_lookup(Invalid, HeaderKey), Headers1. +encrypt_decrypt(_Config) -> + %% Take all available block ciphers. + Hashes = rabbit_pbe:supported_hashes(), + Ciphers = rabbit_pbe:supported_ciphers(), + %% For each cipher, try to encrypt and decrypt data sizes from 0 to 64 bytes + %% with a random passphrase. + _ = [begin + PassPhrase = crypto:strong_rand_bytes(16), + Iterations = rand_compat:uniform(100), + Data = crypto:strong_rand_bytes(64), + [begin + Expected = binary:part(Data, 0, Len), + Enc = rabbit_pbe:encrypt(C, H, Iterations, PassPhrase, Expected), + Expected = iolist_to_binary(rabbit_pbe:decrypt(C, H, Iterations, PassPhrase, Enc)) + end || Len <- lists:seq(0, byte_size(Data))] + end || H <- Hashes, C <- Ciphers], + ok. + +encrypt_decrypt_term(_Config) -> + %% Take all available block ciphers. + Hashes = rabbit_pbe:supported_hashes(), + Ciphers = rabbit_pbe:supported_ciphers(), + %% Different Erlang terms to try encrypting. + DataSet = [ + 10000, + [5672], + [{"127.0.0.1", 5672}, + {"::1", 5672}], + [{connection, info}, {channel, info}], + [{cacertfile, "/path/to/testca/cacert.pem"}, + {certfile, "/path/to/server/cert.pem"}, + {keyfile, "/path/to/server/key.pem"}, + {verify, verify_peer}, + {fail_if_no_peer_cert, false}], + [<<".*">>, <<".*">>, <<".*">>] + ], + _ = [begin + PassPhrase = crypto:strong_rand_bytes(16), + Iterations = rand_compat:uniform(100), + Enc = rabbit_pbe:encrypt_term(C, H, Iterations, PassPhrase, Data), + Data = rabbit_pbe:decrypt_term(C, H, Iterations, PassPhrase, Enc) + end || H <- Hashes, C <- Ciphers, Data <- DataSet], + ok. + +decrypt_config(_Config) -> + %% Take all available block ciphers. + Hashes = rabbit_pbe:supported_hashes(), + Ciphers = rabbit_pbe:supported_ciphers(), + Iterations = [1, 10, 100, 1000], + %% Loop through all hashes, ciphers and iterations. + _ = [begin + PassPhrase = crypto:strong_rand_bytes(16), + do_decrypt_config({C, H, I, PassPhrase}) + end || H <- Hashes, C <- Ciphers, I <- Iterations], + ok. + +do_decrypt_config(Algo = {C, H, I, P}) -> + application:load(rabbit), + RabbitConfig = application:get_all_env(rabbit), + %% Encrypt a few values in configuration. + %% Common cases. + _ = [encrypt_value(Key, Algo) || Key <- [ + tcp_listeners, + num_tcp_acceptors, + ssl_options, + vm_memory_high_watermark, + default_pass, + default_permissions, + cluster_nodes, + auth_mechanisms, + msg_store_credit_disc_bound]], + %% Special case: encrypt a value in a list. + {ok, [LoopbackUser]} = application:get_env(rabbit, loopback_users), + EncLoopbackUser = rabbit_pbe:encrypt_term(C, H, I, P, LoopbackUser), + application:set_env(rabbit, loopback_users, [{encrypted, EncLoopbackUser}]), + %% Special case: encrypt a value in a key/value list. + {ok, TCPOpts} = application:get_env(rabbit, tcp_listen_options), + {_, Backlog} = lists:keyfind(backlog, 1, TCPOpts), + {_, Linger} = lists:keyfind(linger, 1, TCPOpts), + EncBacklog = rabbit_pbe:encrypt_term(C, H, I, P, Backlog), + EncLinger = rabbit_pbe:encrypt_term(C, H, I, P, Linger), + TCPOpts1 = lists:keyreplace(backlog, 1, TCPOpts, {backlog, {encrypted, EncBacklog}}), + TCPOpts2 = lists:keyreplace(linger, 1, TCPOpts1, {linger, {encrypted, EncLinger}}), + application:set_env(rabbit, tcp_listen_options, TCPOpts2), + %% Decrypt configuration. + rabbit:decrypt_config([rabbit], Algo), + %% Check that configuration was decrypted properly. + RabbitConfig = application:get_all_env(rabbit), + application:unload(rabbit), + ok. + +encrypt_value(Key, {C, H, I, P}) -> + {ok, Value} = application:get_env(rabbit, Key), + EncValue = rabbit_pbe:encrypt_term(C, H, I, P, Value), + application:set_env(rabbit, Key, {encrypted, EncValue}). + +decrypt_start_app(Config) -> + do_decrypt_start_app(Config, "hello"). + +decrypt_start_app_file(Config) -> + do_decrypt_start_app(Config, {file, ?config(data_dir, Config) ++ "/rabbit_shovel_test.passphrase"}). + +do_decrypt_start_app(Config, Passphrase) -> + %% Configure rabbit for decrypting configuration. + application:set_env(rabbit, decoder_config, [ + {cipher, aes_cbc256}, + {hash, sha512}, + {iterations, 1000}, + {passphrase, Passphrase} + ]), + %% Add the path to our test application. + code:add_path(?config(data_dir, Config) ++ "/lib/rabbit_shovel_test/ebin"), + %% Attempt to start our test application. + %% + %% We expect a failure *after* the decrypting has been done. + try + rabbit:start_apps([rabbit_shovel_test]) + catch _:_ -> + ok + end, + %% Check if the values have been decrypted. + {ok, Shovels} = application:get_env(rabbit_shovel_test, shovels), + {_, FirstShovel} = lists:keyfind(my_first_shovel, 1, Shovels), + {_, Sources} = lists:keyfind(sources, 1, FirstShovel), + {_, Brokers} = lists:keyfind(brokers, 1, Sources), + ["amqp://fred:secret@host1.domain/my_vhost", + "amqp://john:secret@host2.domain/my_vhost"] = Brokers, + ok. + +decrypt_start_app_undefined(Config) -> + %% Configure rabbit for decrypting configuration. + application:set_env(rabbit, decoder_config, [ + {cipher, aes_cbc256}, + {hash, sha512}, + {iterations, 1000} + %% No passphrase option! + ]), + %% Add the path to our test application. + code:add_path(?config(data_dir, Config) ++ "/lib/rabbit_shovel_test/ebin"), + %% Attempt to start our test application. + %% + %% We expect a failure during decryption because the passphrase is missing. + try + rabbit:start_apps([rabbit_shovel_test]) + catch + exit:{bad_configuration,decoder_config} -> ok; + _:_ -> exit(unexpected_exception) + end. + +decrypt_start_app_wrong_passphrase(Config) -> + %% Configure rabbit for decrypting configuration. + application:set_env(rabbit, decoder_config, [ + {cipher, aes_cbc256}, + {hash, sha512}, + {iterations, 1000}, + {passphrase, "wrong passphrase"} + ]), + %% Add the path to our test application. + code:add_path(?config(data_dir, Config) ++ "/lib/rabbit_shovel_test/ebin"), + %% Attempt to start our test application. + %% + %% We expect a failure during decryption because the passphrase is wrong. + try + rabbit:start_apps([rabbit_shovel_test]) + catch + exit:{decryption_error,_,_} -> ok; + _:_ -> exit(unexpected_exception) + end. + +rabbitmqctl_encode(_Config) -> + % list ciphers and hashes + {ok, _} = rabbit_control_pbe:encode(true, false, undefined, undefined, undefined, undefined, undefined), + {ok, _} = rabbit_control_pbe:encode(false, true, undefined, undefined, undefined, undefined, undefined), + % incorrect ciphers, hashes and iteration number + {error, _} = rabbit_control_pbe:encode(false, false, undefined, funny_cipher, undefined, undefined, undefined), + {error, _} = rabbit_control_pbe:encode(false, false, undefined, undefined, funny_hash, undefined, undefined), + {error, _} = rabbit_control_pbe:encode(false, false, undefined, undefined, undefined, -1, undefined), + {error, _} = rabbit_control_pbe:encode(false, false, undefined, undefined, undefined, 0, undefined), + % incorrect number of arguments + {error, _} = rabbit_control_pbe:encode( + false, false, + false, % encrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [] + ), + {error, _} = rabbit_control_pbe:encode( + false, false, + false, % encrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [undefined] + ), + {error, _} = rabbit_control_pbe:encode( + false, false, + false, % encrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [undefined, undefined, undefined] + ), + + % encrypt/decrypt + % string + rabbitmqctl_encode_encrypt_decrypt("foobar"), + % binary + rabbitmqctl_encode_encrypt_decrypt("<<\"foobar\">>"), + % tuple + rabbitmqctl_encode_encrypt_decrypt("{password,<<\"secret\">>}"), + + ok. + +rabbitmqctl_encode_encrypt_decrypt(Secret) -> + PassPhrase = "passphrase", + {ok, Output} = rabbit_control_pbe:encode( + false, false, + false, % encrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [Secret, PassPhrase] + ), + {encrypted, Encrypted} = rabbit_control_pbe:evaluate_input_as_term(lists:flatten(Output)), + + {ok, Result} = rabbit_control_pbe:encode( + false, false, + true, % decrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [lists:flatten(io_lib:format("~p", [Encrypted])), PassPhrase] + ), + Secret = lists:flatten(Result), + % decrypt with {encrypted, ...} form as input + {ok, Result} = rabbit_control_pbe:encode( + false, false, + true, % decrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [lists:flatten(io_lib:format("~p", [{encrypted, Encrypted}])), PassPhrase] + ), + + % wrong passphrase + {error, _} = rabbit_control_pbe:encode( + false, false, + true, % decrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [lists:flatten(io_lib:format("~p", [Encrypted])), PassPhrase ++ " "] + ), + {error, _} = rabbit_control_pbe:encode( + false, false, + true, % decrypt + rabbit_pbe:default_cipher(), rabbit_pbe:default_hash(), rabbit_pbe:default_iterations(), + [lists:flatten(io_lib:format("~p", [{encrypted, Encrypted}])), PassPhrase ++ " "] + ) + . + %% ------------------------------------------------------------------- %% pg_local. %% ------------------------------------------------------------------- diff --git a/test/unit_SUITE_data/lib/rabbit_shovel_test/ebin/rabbit_shovel_test.app b/test/unit_SUITE_data/lib/rabbit_shovel_test/ebin/rabbit_shovel_test.app new file mode 100644 index 0000000000..a8481c9aa4 --- /dev/null +++ b/test/unit_SUITE_data/lib/rabbit_shovel_test/ebin/rabbit_shovel_test.app @@ -0,0 +1,46 @@ +{application, rabbit_shovel_test, + [{description, "Test .app file for tests for encrypting configuration"}, + {vsn, ""}, + {modules, []}, + {env, [ {shovels, [ {my_first_shovel, + [ {sources, + [ {brokers, [ {encrypted, <<"CfJXuka/uJYsqAtiJnwKpSY4moMPcOBh4sO8XDcdmhXbVYGKCDLKEilWPMfvOAQ2lN1BQneGn6bvDZi2+gDu6iHVKfafQAZSv8zcsVB3uYdBXFzqTCWO8TAsgG6LUMPT">>} + , {encrypted, <<"dBO6n+G1OiBwZeLXhvmNYeTE57nhBOmicUBF34zo4nQjerzQaNoEk8GA2Ts5PzMhYeO6U6Y9eEmheqIr9Gzh2duLZic65ZMQtIKNpWcZJllEhGpk7aV1COr23Yur9fWG">>} + ]} + , {declarations, [ {'exchange.declare', + [ {exchange, <<"my_fanout">>} + , {type, <<"fanout">>} + , durable + ]} + , {'queue.declare', + [{arguments, + [{<<"x-message-ttl">>, long, 60000}]}]} + , {'queue.bind', + [ {exchange, <<"my_direct">>} + , {queue, <<>>} + ]} + ]} + ]} + , {destinations, + [ {broker, "amqp://"} + , {declarations, [ {'exchange.declare', + [ {exchange, <<"my_direct">>} + , {type, <<"direct">>} + , durable + ]} + ]} + ]} + , {queue, <<>>} + , {prefetch_count, 10} + , {ack_mode, on_confirm} + , {publish_properties, [ {delivery_mode, 2} ]} + , {add_forward_headers, true} + , {publish_fields, [ {exchange, <<"my_direct">>} + , {routing_key, <<"from_shovel">>} + ]} + , {reconnect_delay, 5} + ]} + ]} + ]}, + + {applications, [kernel, stdlib]}]}. diff --git a/test/unit_SUITE_data/rabbit_shovel_test.passphrase b/test/unit_SUITE_data/rabbit_shovel_test.passphrase new file mode 100644 index 0000000000..ce01362503 --- /dev/null +++ b/test/unit_SUITE_data/rabbit_shovel_test.passphrase @@ -0,0 +1 @@ +hello |
