Răsfoiți Sursa

Merge branch 'main' into config-command-line.

Dan Helfman 2 luni în urmă
părinte
comite
3f25f3f0ff

+ 1 - 0
NEWS

@@ -32,6 +32,7 @@
    "working_directory" are used.
  * #1044: Fix an error in the systemd credential hook when the credential name contains a "."
    character.
+ * #1047: Add "key-file" and "yubikey" options to the KeePassXC credential hook.
  * #1048: Fix a "no such file or directory" error in ZFS, Btrfs, and LVM hooks with nested
    directories that reside on separate devices/filesystems.
  * #1050: Fix a failure in the "spot" check when the archive contains a symlink.

+ 14 - 0
borgmatic/config/schema.yaml

@@ -2790,6 +2790,20 @@ properties:
                 description: |
                     Command to use instead of "keepassxc-cli".
                 example: /usr/local/bin/keepassxc-cli
+            key_file:
+                type: string
+                description: |
+                    Path to a key file for unlocking the KeePassXC database.
+                example: /path/to/keyfile
+            yubikey:
+                type: string
+                description: |
+                    YubiKey slot and optional serial number used to access the
+                    KeePassXC database. The format is "<slot[:serial]>", where:
+                     * <slot> is the YubiKey slot number (e.g., `1` or `2`).
+                     * <serial> (optional) is the YubiKey's serial number (e.g.,
+                       `7370001`).
+                example: "1:7370001"
         description: |
             Configuration for integration with the KeePassXC password manager.
     default_actions:

+ 18 - 17
borgmatic/hooks/credential/keepassxc.py

@@ -11,34 +11,35 @@ 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 corresponidng KeePassXC credential and return it.
+    the corresponding KeePassXC credential and return it.
 
     Raise ValueError if keepassxc-cli can't retrieve the credential.
     '''
     try:
         (database_path, attribute_name) = credential_parameters
     except ValueError:
-        path_and_name = ' '.join(credential_parameters)
-
-        raise ValueError(
-            f'Cannot load credential with invalid KeePassXC database path and attribute name: "{path_and_name}"'
-        )
+        raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
 
     expanded_database_path = os.path.expanduser(database_path)
 
     if not os.path.exists(expanded_database_path):
-        raise ValueError(
-            f'Cannot load credential because KeePassXC database path does not exist: {database_path}'
-        )
+        raise ValueError(f'KeePassXC database path does not exist: {database_path}')
 
-    return borgmatic.execute.execute_command_and_capture_output(
+    # Build the keepassxc-cli command.
+    command = (
         tuple(shlex.split((hook_config or {}).get('keepassxc_cli_command', 'keepassxc-cli')))
+        + ('show', '--show-protected', '--attributes', 'Password')
         + (
-            'show',
-            '--show-protected',
-            '--attributes',
-            'Password',
-            expanded_database_path,
-            attribute_name,
+            ('--key-file', hook_config['key_file'])
+            if hook_config and hook_config.get('key_file')
+            else ()
         )
-    ).rstrip(os.linesep)
+        + (
+            ('--yubikey', hook_config['yubikey'])
+            if hook_config and hook_config.get('yubikey')
+            else ()
+        )
+        + (expanded_database_path, attribute_name)  # Ensure database and entry are last.
+    )
+
+    return borgmatic.execute.execute_command_and_capture_output(command).rstrip(os.linesep)

+ 101 - 0
tests/unit/hooks/credential/test_keepassxc.py

@@ -116,3 +116,104 @@ def test_load_credential_with_expanded_directory_with_present_database_fetches_p
         )
         == 'password'
     )
+
+
+def test_load_credential_with_key_file():
+    flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
+        'database.kdbx'
+    )
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.borgmatic.execute).should_receive(
+        'execute_command_and_capture_output'
+    ).with_args(
+        (
+            'keepassxc-cli',
+            'show',
+            '--show-protected',
+            '--attributes',
+            'Password',
+            '--key-file',
+            '/path/to/keyfile',
+            'database.kdbx',
+            'mypassword',
+        )
+    ).and_return(
+        'password'
+    ).once()
+
+    assert (
+        module.load_credential(
+            hook_config={'key_file': '/path/to/keyfile'},
+            config={},
+            credential_parameters=('database.kdbx', 'mypassword'),
+        )
+        == 'password'
+    )
+
+
+def test_load_credential_with_yubikey():
+    flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
+        'database.kdbx'
+    )
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.borgmatic.execute).should_receive(
+        'execute_command_and_capture_output'
+    ).with_args(
+        (
+            'keepassxc-cli',
+            'show',
+            '--show-protected',
+            '--attributes',
+            'Password',
+            '--yubikey',
+            '1:7370001',
+            'database.kdbx',
+            'mypassword',
+        )
+    ).and_return(
+        'password'
+    ).once()
+
+    assert (
+        module.load_credential(
+            hook_config={'yubikey': '1:7370001'},
+            config={},
+            credential_parameters=('database.kdbx', 'mypassword'),
+        )
+        == 'password'
+    )
+
+
+def test_load_credential_with_key_file_and_yubikey():
+    flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
+        'database.kdbx'
+    )
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.borgmatic.execute).should_receive(
+        'execute_command_and_capture_output'
+    ).with_args(
+        (
+            'keepassxc-cli',
+            'show',
+            '--show-protected',
+            '--attributes',
+            'Password',
+            '--key-file',
+            '/path/to/keyfile',
+            '--yubikey',
+            '2',
+            'database.kdbx',
+            'mypassword',
+        )
+    ).and_return(
+        'password'
+    ).once()
+
+    assert (
+        module.load_credential(
+            hook_config={'key_file': '/path/to/keyfile', 'yubikey': '2'},
+            config={},
+            credential_parameters=('database.kdbx', 'mypassword'),
+        )
+        == 'password'
+    )