diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decode_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decode_command.ex index 9f4211d894..522c387e67 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decode_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decode_command.ex @@ -117,7 +117,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.DecodeCommand do def formatter(), do: RabbitMQ.CLI.Formatters.Erlang def banner(_, _) do - "Decrypting value..." + "Decrypting an advanced.config (Erlang term) value..." end def usage, @@ -125,7 +125,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.DecodeCommand do def usage_additional() do [ - ["", "config value to decode"], + ["", "advanced.config (Erlang term) value to decode"], ["", "passphrase to use with the config value encryption key"], ["--cipher ", "cipher suite to use"], ["--hash ", "hashing function to use"], @@ -141,7 +141,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.DecodeCommand do def help_section(), do: :configuration - def description(), do: "Decrypts an encrypted configuration value" + def description(), do: "Decrypts an encrypted advanced.config value" # # Implementation diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decrypt_conf_value_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decrypt_conf_value_command.ex new file mode 100644 index 0000000000..6ac5958a96 --- /dev/null +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/decrypt_conf_value_command.ex @@ -0,0 +1,172 @@ +## This Source Code Form is subject to the terms of the Mozilla Public +## License, v. 2.0. If a copy of the MPL was not distributed with this +## file, You can obtain one at https://mozilla.org/MPL/2.0/. +## +## Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +alias RabbitMQ.CLI.Core.Helpers + +defmodule RabbitMQ.CLI.Ctl.Commands.DecryptConfValueCommand do + alias RabbitMQ.CLI.Core.{DocGuide, Input} + + @behaviour RabbitMQ.CLI.CommandBehaviour + use RabbitMQ.CLI.DefaultOutput + + def switches() do + [ + cipher: :string, + hash: :string, + iterations: :integer + ] + end + + @atomized_keys [:cipher, :hash] + @prefix "encrypted:" + + def distribution(_), do: :none + + def merge_defaults(args, opts) do + with_defaults = + Map.merge( + %{ + cipher: :rabbit_pbe.default_cipher(), + hash: :rabbit_pbe.default_hash(), + iterations: :rabbit_pbe.default_iterations() + }, + opts + ) + + {args, Helpers.atomize_values(with_defaults, @atomized_keys)} + end + + def validate(args, _) when length(args) < 1 do + {:validation_failure, {:not_enough_args, "Please provide a value to decode and a passphrase"}} + end + + def validate(args, _) when length(args) > 2 do + {:validation_failure, :too_many_args} + end + + def validate(_args, opts) do + case {supports_cipher(opts.cipher), supports_hash(opts.hash), opts.iterations > 0} do + {false, _, _} -> + {:validation_failure, {:bad_argument, "The requested cipher is not supported"}} + + {_, false, _} -> + {:validation_failure, {:bad_argument, "The requested hash is not supported"}} + + {_, _, false} -> + {:validation_failure, + {:bad_argument, + "The requested number of iterations is incorrect (must be a positive integer)"}} + + {true, true, true} -> + :ok + end + end + + def run([value], %{cipher: cipher, hash: hash, iterations: iterations} = opts) do + case Input.consume_single_line_string_with_prompt("Passphrase: ", opts) do + :eof -> + {:error, :not_enough_args} + + passphrase -> + try do + term_value = Helpers.evaluate_input_as_term(value) + + term_to_decrypt = + case term_value do + prefixed_val when is_bitstring(prefixed_val) or is_list(prefixed_val) -> + tag_input_value_with_encrypted(prefixed_val) + + {:encrypted, _} = encrypted -> + encrypted + + _ -> + {:encrypted, term_value} + end + + result = :rabbit_pbe.decrypt_term(cipher, hash, iterations, passphrase, term_to_decrypt) + {:ok, result} + catch + _, _ -> + IO.inspect(__STACKTRACE__) + {:error, + "Failed to decrypt the value. Things to check: is the passphrase correct? Are the cipher and hash algorithms the same as those used for encryption?"} + end + end + end + + def run([value, passphrase], %{cipher: cipher, hash: hash, iterations: iterations}) do + try do + term_value = Helpers.evaluate_input_as_term(value) + + term_to_decrypt = + case term_value do + prefixed_val when is_bitstring(prefixed_val) or is_list(prefixed_val) -> + tag_input_value_with_encrypted(prefixed_val) + + {:encrypted, _} = encrypted -> + encrypted + + _ -> + {:encrypted, term_value} + end + + result = :rabbit_pbe.decrypt_term(cipher, hash, iterations, passphrase, term_to_decrypt) + {:ok, result} + catch + _, _ -> + IO.inspect(__STACKTRACE__) + {:error, + "Failed to decrypt the value. Things to check: is the passphrase correct? Are the cipher and hash algorithms the same as those used for encryption?"} + end + end + + def formatter(), do: RabbitMQ.CLI.Formatters.Erlang + + def banner(_, _) do + "Decrypting a rabbitmq.conf string value..." + end + + def usage, + do: "decrypt_conf_value value passphrase [--cipher ] [--hash ] [--iterations ]" + + def usage_additional() do + [ + ["", "a double-quoted rabbitmq.conf string value to decode"], + ["", "passphrase to use with the config value encryption key"], + ["--cipher ", "cipher suite to use"], + ["--hash ", "hashing function to use"], + ["--iterations ", "number of iteration to apply"] + ] + end + + def usage_doc_guides() do + [ + DocGuide.configuration() + ] + end + + def help_section(), do: :configuration + + def description(), do: "Decrypts an encrypted configuration value" + + # + # Implementation + # + + defp supports_cipher(cipher), do: Enum.member?(:rabbit_pbe.supported_ciphers(), cipher) + + defp supports_hash(hash), do: Enum.member?(:rabbit_pbe.supported_hashes(), hash) + + defp tag_input_value_with_encrypted(value) when is_bitstring(value) or is_list(value) do + bin_val = :rabbit_data_coercion.to_binary(value) + untagged_val = String.replace_prefix(bin_val, @prefix, "") + + {:encrypted, untagged_val} + end + defp tag_input_value_with_encrypted(value) do + {:encrypted, value} + end +end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encode_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encode_command.ex index 368bbd78c2..03615420df 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encode_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encode_command.ex @@ -130,7 +130,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.EncodeCommand do def usage_additional() do [ - ["", "config value to encode"], + ["", "value to encode, to be used in advanced.config"], ["", "passphrase to use with the config value encryption key"], ["--cipher ", "cipher suite to use"], ["--hash ", "hashing function to use"], diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encrypt_conf_value_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encrypt_conf_value_command.ex index f330f77327..71809267bc 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encrypt_conf_value_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/encrypt_conf_value_command.ex @@ -2,7 +2,7 @@ ## License, v. 2.0. If a copy of the MPL was not distributed with this ## file, You can obtain one at https://mozilla.org/MPL/2.0/. ## -## Copyright (c) 2007-2023 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. +## Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. defmodule RabbitMQ.CLI.Ctl.Commands.EncryptConfValueCommand do alias RabbitMQ.CLI.Core.{DocGuide, Helpers, Input} diff --git a/deps/rabbitmq_cli/test/ctl/encrypt_conf_value_command_test.exs b/deps/rabbitmq_cli/test/ctl/encrypt_conf_value_command_test.exs new file mode 100644 index 0000000000..e65f3b99a2 --- /dev/null +++ b/deps/rabbitmq_cli/test/ctl/encrypt_conf_value_command_test.exs @@ -0,0 +1,78 @@ +## This Source Code Form is subject to the terms of the Mozilla Public +## License, v. 2.0. If a copy of the MPL was not distributed with this +## file, You can obtain one at https://mozilla.org/MPL/2.0/. +## +## Copyright (c) 2007-2024 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +defmodule EncryptConfValueCommandTest do + use ExUnit.Case, async: false + + @command RabbitMQ.CLI.Ctl.Commands.EncryptConfValueCommand + + setup _context do + {:ok, + opts: %{ + cipher: :rabbit_pbe.default_cipher(), + hash: :rabbit_pbe.default_hash(), + iterations: :rabbit_pbe.default_iterations() + }} + end + + test "validate: providing exactly 2 positional arguments passes", context do + assert :ok == @command.validate(["value", "secret"], context[:opts]) + end + + test "validate: providing zero or one positional argument passes", context do + assert :ok == @command.validate([], context[:opts]) + assert :ok == @command.validate(["value"], context[:opts]) + end + + test "validate: providing three or more positional argument fails", context do + assert match?( + {:validation_failure, :too_many_args}, + @command.validate(["value", "secret", "incorrect"], context[:opts]) + ) + end + + test "validate: hash and cipher must be supported", context do + assert match?( + {:validation_failure, {:bad_argument, _}}, + @command.validate( + ["value", "secret"], + Map.merge(context[:opts], %{cipher: :funny_cipher}) + ) + ) + + assert match?( + {:validation_failure, {:bad_argument, _}}, + @command.validate( + ["value", "secret"], + Map.merge(context[:opts], %{hash: :funny_hash}) + ) + ) + + assert match?( + {:validation_failure, {:bad_argument, _}}, + @command.validate( + ["value", "secret"], + Map.merge(context[:opts], %{cipher: :funny_cipher, hash: :funny_hash}) + ) + ) + + assert :ok == @command.validate(["value", "secret"], context[:opts]) + end + + test "validate: number of iterations must greater than 0", context do + assert match?( + {:validation_failure, {:bad_argument, _}}, + @command.validate(["value", "secret"], Map.merge(context[:opts], %{iterations: 0})) + ) + + assert match?( + {:validation_failure, {:bad_argument, _}}, + @command.validate(["value", "secret"], Map.merge(context[:opts], %{iterations: -1})) + ) + + assert :ok == @command.validate(["value", "secret"], context[:opts]) + end +end