diff --git a/docs/guide/vaults.md b/docs/guide/vaults.md index 771b8f1e..5bf189e6 100644 --- a/docs/guide/vaults.md +++ b/docs/guide/vaults.md @@ -167,7 +167,7 @@ A flexible HTTP vault that speaks one of three wire protocols, selected with `ap password: "your-api-token" ``` - Requests are `POST`ed as `{"method": ..., "params": ..., "token": ...}` using methods like `GetKey`, `InsertKey`, and `GetServices`. This mode cannot list all keys in bulk, so a `kv copy` from a json vault relies on the remote server's own duplicate handling. + Requests are `POST`ed as `{"method": ..., "params": ..., "token": ...}` using methods like `GetKey`, `InsertKey`, and `GetServices`. Bulk writes (`kv copy`, `kv add`) first try `InsertKeys` with `params: {"keys": {kid: key, ...}, "service": ..., "title": ...}` (up to 500 content keys per call) and expect `{"inserted": }` back. A server that answers HTTP 404 to `InsertKeys` gets one `InsertKey` call per content key for the rest of the run instead. This mode cannot list all keys in bulk, so a `kv copy` from a json vault relies on the remote server's own duplicate handling. === "decrypt_labs mode" diff --git a/unshackle/commands/kv.py b/unshackle/commands/kv.py index 2491a354..fc8cc828 100644 --- a/unshackle/commands/kv.py +++ b/unshackle/commands/kv.py @@ -57,7 +57,7 @@ def process_service_keys(from_vault: Vault, service: str, log: logging.Logger) - def add_keys_with_progress(vault: Vault, service: str, kid_keys: dict[str, str], log: logging.Logger) -> int: """Add content keys to a vault in batches, with a progress bar of counts and time left.""" - chunk = 1 if type(vault).__name__ == "HTTP" else 500 + chunk = 1 if type(vault).__name__ == "HTTP" and getattr(vault, "api_mode", None) != "json" else 500 kids = list(kid_keys) added = 0 console.print() diff --git a/unshackle/vaults/HTTP.py b/unshackle/vaults/HTTP.py index 6d3235f3..3a79cdf6 100644 --- a/unshackle/vaults/HTTP.py +++ b/unshackle/vaults/HTTP.py @@ -57,6 +57,7 @@ class HTTP(Vault): self.username = username self.api_mode = api_mode.lower() + self.batch_insert = True self.current_title = None self.session = Session() self.session.headers.update({"User-Agent": f"unshackle v{__version__}"}) @@ -278,6 +279,18 @@ class HTTP(Vault): title = getattr(self, "current_title", None) if self.api_mode == "json": + if self.batch_insert: + try: + response = self.request( + "InsertKeys", {"keys": processed_kid_keys, "service": service.lower(), "title": title} + ) + if response.get("status") != "not_found": + return int(response.get("inserted", 0)) + # only a 404 proves the server has no InsertKeys method; other + # failures may be transient, so keep batching for the next call + self.batch_insert = False + except Exception: + pass for kid, key in processed_kid_keys.items(): try: response = self.request(