diff --git a/lib/kamal/secrets/adapters/keepassxc.rb b/lib/kamal/secrets/adapters/keepassxc.rb new file mode 100644 index 000000000..c1080d144 --- /dev/null +++ b/lib/kamal/secrets/adapters/keepassxc.rb @@ -0,0 +1,43 @@ +require "open3" +require "io/console" + +class Kamal::Secrets::Adapters::Keepassxc < Kamal::Secrets::Adapters::Base + # Usage Example: + # kamal secrets fetch --adapter keepassxc --account ~/path/to/secrets.kdbx --from entry-title KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY ANY_OTHER_ATTRIBUTE_SAVED_IN_ADVANCE_TAB_OF_AN_ENTRY + + private + + def check_dependencies! + raise "KeePassXC CLI is not installed." unless cli_installed? + end + + def login(account) + ask_for_password(account) + end + + def fetch_secrets(secrets, from:, account:, session:) + secrets.each_with_object({}) do |secret, results| + # If asking for "password", use standard field, otherwise use Attribute lookup + attr_flag = (secret == "password") ? [] : ["-a", secret] + results[secret] = run_command("show", account, from, *attr_flag, "-q", "--show-protected", session: session) + end + end + + def cli_installed? + `keepassxc-cli --version 2> /dev/null` + $?.success? + end + + def ask_for_password(account) + File.open("/dev/tty", "r+") do |tty| + tty.getpass("Enter KeePassXC Master Password for #{File.basename(account)}: ") + end + end + + def run_command(*args, session:) + cmd = ["keepassxc-cli", *args] + stdout, stderr, status = Open3.capture3(*cmd, stdin_data: session) + raise "KeePassXC Error: #{stderr.strip}" unless status.success? + stdout.strip + end +end diff --git a/test/secrets/keepassxc_adapter_test.rb b/test/secrets/keepassxc_adapter_test.rb new file mode 100644 index 000000000..4049256da --- /dev/null +++ b/test/secrets/keepassxc_adapter_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class KeepassxcAdapterTest < SecretAdapterTestCase + setup do + @keepassxc = Kamal::Secrets::Adapters::Keepassxc.new + end + + test "fetch via CLI" do + # Simulate when CLI is installed + stub_ticks_with("keepassxc-cli --version 2> /dev/null", succeed: true) + + @keepassxc.stub :ask_for_password, "dummy_pass" do + Open3.stubs(:capture3).with("keepassxc-cli", "show", "/tmp/secrets.kdbx", "test-env", "-a", "MY_SECRET", "-q", "--show-protected", stdin_data: "dummy_pass") + .returns([ "cli_value", "", mock(success?: true) ]) + + secrets = @keepassxc.fetch([ "MY_SECRET" ], account: "/tmp/secrets.kdbx", from: "test-env") + assert_equal({ "MY_SECRET" => "cli_value" }, secrets) + end + end + + test "check_dependencies! raises if CLI is missing" do + stub_ticks_with("keepassxc-cli --version 2> /dev/null", succeed: false) + + error = assert_raises(RuntimeError) do + @keepassxc.send(:check_dependencies!) + end + assert_match(/KeePassXC CLI is not installed/, error.message) + end +end