From a508cabe3fcb847f6772e77f34cd74de26cd32fd Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 29 Apr 2026 12:52:27 -0700 Subject: [PATCH] Update the KeePassXC credential hook to support KeePassXC's secret service integration. --- NEWS | 3 ++ borgmatic/config/schema.yaml | 12 ++++- borgmatic/hooks/credential/keepassxc.py | 19 ++++++- .../configuration/credentials/keepassxc.md | 34 +++++++++++-- tests/unit/hooks/credential/test_keepassxc.py | 49 +++++++++++++++++++ 5 files changed, 110 insertions(+), 7 deletions(-) diff --git a/NEWS b/NEWS index 4a80fbeb..da578889 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,9 @@ "working_directory". * #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes and globs. + * Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the + documentation for more information: + https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/ * Add the NEWS changelog file to release tarball (#1298). * Enable reply by email on projects.torsion.org, so replies to notification emails get posted as comments on tickets. diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 374ebf34..f0fc45e4 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -3309,24 +3309,32 @@ properties: description: | Command to use instead of "keepassxc-cli". example: /usr/local/bin/keepassxc-cli + secret_tool_command: + type: string + description: | + Command to use instead of "secret-tool". + example: /usr/local/bin/secret-tool ask_for_password: type: boolean description: | Whether keepassxc-cli should prompt the user for a password. Disabling this is only really useful if you're unlocking your KeePassXC database with a key file instead of a - password. Defaults to true. + password. Ignored when using KeePassXC's secret service + integration. Defaults to true. example: false key_file: type: string description: | Path to a key file for unlocking the KeePassXC database. + Ignored when using KeePassXC's secret service integration. example: /path/to/keyfile yubikey: type: string description: | YubiKey slot and optional serial number used to access the - KeePassXC database. The format is "", where: + KeePassXC database. Ignored when using KeePassXC's secret + service integration. The format is "", where: * is the YubiKey slot number (e.g., `1` or `2`). * (optional) is the YubiKey's serial number (e.g., `7370001`). diff --git a/borgmatic/hooks/credential/keepassxc.py b/borgmatic/hooks/credential/keepassxc.py index 2687b038..9bdac9d4 100644 --- a/borgmatic/hooks/credential/keepassxc.py +++ b/borgmatic/hooks/credential/keepassxc.py @@ -7,19 +7,34 @@ import borgmatic.execute logger = logging.getLogger(__name__) +SECRET_SERVICE_DATABASE_PATH = 'secret-service' + + def load_credential(hook_config, config, credential_parameters): ''' Given the hook configuration dict, the configuration dict, and a credential parameters tuple containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch - the corresponding KeePassXC credential and return it. + the corresponding KeePassXC credential and return it. Or use secret-tool if the database path + is "secret-service", indicating that KeePassXC's secret service integration should be used + instead. - Raise ValueError if keepassxc-cli can't retrieve the credential. + Raise ValueError if keepassxc-cli or secret-tool can't retrieve the credential. ''' try: (database_path, attribute_name) = credential_parameters except ValueError: raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}') + if database_path == SECRET_SERVICE_DATABASE_PATH: + command = ( + *shlex.split((hook_config or {}).get('secret_tool_command', 'secret-tool')), + *('lookup', 'Path', attribute_name), + ) + + return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip( + os.linesep + ) + expanded_database_path = os.path.expanduser(database_path) if not os.path.exists(expanded_database_path): diff --git a/docs/reference/configuration/credentials/keepassxc.md b/docs/reference/configuration/credentials/keepassxc.md index ca88cee8..08dc6619 100644 --- a/docs/reference/configuration/credentials/keepassxc.md +++ b/docs/reference/configuration/credentials/keepassxc.md @@ -43,14 +43,15 @@ postgresql_databases: ``` -### Custom command +### Custom commands -You can also optionally override the `keepassxc-cli` command that borgmatic calls to load -passwords: +You can also optionally override the `keepassxc-cli` or `secret-tool` commands +that borgmatic call to load passwords: ```yaml keepassxc: keepassxc_cli_command: /usr/local/bin/keepassxc-cli + secret_tool_command: /usr/local/bin/secret-tool ``` Another example: @@ -98,3 +99,30 @@ keepassxc: The value here is the YubiKey slot number (e.g., `1` or `2`) and optional serial number (e.g., `7370001`) used to access the KeePassXC database. Join the two values with a colon, but omit the colon if you're leaving out the serial number. + + +### Secret service integration + +New in version 2.1.6 borgmatic +supports [KeePassXC's secret service +integration](https://keepassxc.org/docs/KeePassXC_UserGuide#_secret_service_integration) +that integrates with the [freedesktop secret service +API](https://specifications.freedesktop.org/secret-service/latest/) and allows +clients like borgmatic to access your passwords. + +To use this feature from borgmatic, specify `secret-service` instead of a +KeePassXC database path when calling this credential hook. For instance: + +```yaml +encryption_passphrase: "{credential keepassxc secret-service borgmatic}" +``` + +With this in place, borgmatic runs libsecret's `secret-tool` command to retrieve +the password titled "borgmatic" on demand. KeePassXC may then prompt you to +approve the password access, depending on how you've configured it. + +One benefit of using the KeePassXC's secret service integration like this is +that you don't have to type a KeePassXC database passhprase (or use a keyfile) +whenever borgmatic accesses your passwords. Instead, you can configure +KeePassXC to prompt you to approve or deny each access. Or you can even +pre-approve password access to support automated borgmatic runs. diff --git a/tests/unit/hooks/credential/test_keepassxc.py b/tests/unit/hooks/credential/test_keepassxc.py index dd8ff24c..e2006543 100644 --- a/tests/unit/hooks/credential/test_keepassxc.py +++ b/tests/unit/hooks/credential/test_keepassxc.py @@ -31,6 +31,55 @@ def test_load_credential_with_missing_database_raises(): ) +def test_load_credential_with_secret_service_database_path_fetches_password_via_secret_tool(): + flexmock(module.os.path).should_receive('expanduser').never() + flexmock(module.os.path).should_receive('exists').never() + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output', + ).with_args( + ( + 'secret-tool', + 'lookup', + 'Path', + 'mypassword', + ), + ).and_yield('password').once() + + assert ( + module.load_credential( + hook_config={}, + config={}, + credential_parameters=('secret-service', 'mypassword'), + ) + == 'password' + ) + + +def test_load_credential_with_secret_service_database_path_and_secret_tool_command_calls_it(): + flexmock(module.os.path).should_receive('expanduser').never() + flexmock(module.os.path).should_receive('exists').never() + flexmock(module.borgmatic.execute).should_receive( + 'execute_command_and_capture_output', + ).with_args( + ( + '/usr/local/bin/secret-tool', + '--some-option', + 'lookup', + 'Path', + 'mypassword', + ), + ).and_yield('password').once() + + assert ( + module.load_credential( + hook_config={'secret_tool_command': '/usr/local/bin/secret-tool --some-option'}, + config={}, + credential_parameters=('secret-service', 'mypassword'), + ) + == 'password' + ) + + def test_load_credential_with_present_database_fetches_password_from_keepassxc(): flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return( 'database.kdbx',