53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
# Copyright 2026 Umbra Tools (umbratools.com)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from typing import Protocol
|
|
|
|
|
|
class PromptCancelled(RuntimeError):
|
|
"""Raised when the user cancels a native credential prompt."""
|
|
|
|
|
|
class SecretPrompt(Protocol):
|
|
def __call__(self, profile: str, purpose: str) -> str | None:
|
|
"""Return a secret from a local UI, or None when cancelled."""
|
|
|
|
|
|
def _apple_string(value: str) -> str:
|
|
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
|
|
|
|
def macos_secret_prompt(profile: str, purpose: str) -> str | None:
|
|
"""Open a native macOS hidden-password dialog.
|
|
|
|
The returned value is intended to be passed directly to the Keychain
|
|
backend. It is never printed, logged, or included in an MCP result.
|
|
"""
|
|
message = f"Enter the password for {purpose} ({profile})."
|
|
script = (
|
|
f'display dialog {_apple_string(message)} '
|
|
'default answer "" with hidden answer buttons {"Cancel", "Save"} '
|
|
'default button "Save" with title "UmbraTrans secure credential"'
|
|
)
|
|
try:
|
|
result = subprocess.run(
|
|
["/usr/bin/osascript", "-e", script],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
detail = exc.stderr.strip().lower()
|
|
if "user canceled" in detail or "-128" in detail:
|
|
raise PromptCancelled("Credential entry was cancelled") from exc
|
|
raise RuntimeError("The macOS credential dialog could not be completed") from exc
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
raise RuntimeError("The macOS credential dialog could not be opened") from exc
|
|
|
|
secret = result.stdout.rstrip("\r\n")
|
|
return secret or None
|