diff options
Diffstat (limited to 'src')
| -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 |
5 files changed, 398 insertions, 3 deletions
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). |
