add private macOS password storage skill
This commit is contained in:
parent
cf796fea76
commit
d6fae7be3c
6 changed files with 787 additions and 0 deletions
63
.agents/skills/private-password-storage/SKILL.md
Normal file
63
.agents/skills/private-password-storage/SKILL.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
name: private-password-storage
|
||||||
|
description: Store or update a private password or credential in the device-local macOS Keychain through a native secure popup, and check presence without retrieval. Use when a user asks to save, store, update, or verify a private credential securely on a Mac.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Private Password Storage
|
||||||
|
|
||||||
|
Use the bundled helper so the credential enters only through a native
|
||||||
|
`NSSecureTextField` and goes directly to Security.framework.
|
||||||
|
|
||||||
|
## Choose The Right Authentication Route
|
||||||
|
|
||||||
|
Prefer the provider's supported credential manager or login flow when one
|
||||||
|
exists. Keychain storage does not authenticate an application and cannot
|
||||||
|
satisfy OAuth, OTP, passkeys, browser sessions, or `gcloud` login by itself.
|
||||||
|
|
||||||
|
Never ask the user to paste a credential into chat.
|
||||||
|
|
||||||
|
## Store Or Update
|
||||||
|
|
||||||
|
Run `--store` only after the user asks to open the secure popup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.agents/skills/private-password-storage/scripts/private-password-storage \
|
||||||
|
--service "com.example.application" \
|
||||||
|
--account "operator@example.invalid" \
|
||||||
|
--label "Example application password" \
|
||||||
|
--store
|
||||||
|
```
|
||||||
|
|
||||||
|
The popup accepts normal typing, Cmd+V, and right-click Paste. Empty input
|
||||||
|
stays in the popup and is rejected. A successful write prints only `stored`.
|
||||||
|
The item is stored as `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` with
|
||||||
|
synchronization disabled.
|
||||||
|
|
||||||
|
## Check Presence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.agents/skills/private-password-storage/scripts/private-password-storage \
|
||||||
|
--service "com.example.application" \
|
||||||
|
--account "operator@example.invalid" \
|
||||||
|
--status
|
||||||
|
```
|
||||||
|
|
||||||
|
Status prints only `present` or `absent`. It never returns the credential.
|
||||||
|
|
||||||
|
## Safety Contract
|
||||||
|
|
||||||
|
- Keep the credential out of process arguments, shell variables, files,
|
||||||
|
logs, screenshots, terminal output, clipboard writes, and proof artifacts.
|
||||||
|
- Do not add retrieval, reveal, echo, export, or fingerprint modes.
|
||||||
|
- Do not use AppleScript, `osascript`, System Events, `pbcopy`, or the
|
||||||
|
`security add-generic-password` CLI.
|
||||||
|
- Treat service, account, and label as non-secret metadata; do not print them.
|
||||||
|
- Report only the helper status and whether a native popup was used.
|
||||||
|
|
||||||
|
## Sanitized Canary
|
||||||
|
|
||||||
|
Use only a generated fake value and a service beginning with
|
||||||
|
`dev.codex.private-password-storage.canary.` plus an account beginning with
|
||||||
|
`canary-`. The test-only `--delete-test-item` action is prefix-guarded and
|
||||||
|
deletes only that exact service/account pair. Verify `present`, delete the
|
||||||
|
exact item, then verify `absent`. Do not retain the fake value anywhere.
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
interface:
|
||||||
|
display_name: "Private Password Storage"
|
||||||
|
short_description: "Store device-local credentials securely"
|
||||||
|
default_prompt: "Use $private-password-storage to store or update a private credential securely in the macOS Keychain."
|
||||||
5
.agents/skills/private-password-storage/scripts/private-password-storage
Executable file
5
.agents/skills/private-password-storage/scripts/private-password-storage
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
exec python3 "$SCRIPT_DIR/private_password_storage.py" "$@"
|
||||||
191
.agents/skills/private-password-storage/scripts/private_password_storage.py
Executable file
191
.agents/skills/private-password-storage/scripts/private_password_storage.py
Executable file
|
|
@ -0,0 +1,191 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Launch the native macOS Keychain helper without accepting secret arguments."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import plistlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class SafeArgumentParser(argparse.ArgumentParser):
|
||||||
|
def error(self, message: str) -> None:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = SafeArgumentParser(allow_abbrev=False)
|
||||||
|
parser.add_argument("--service", required=True)
|
||||||
|
parser.add_argument("--account", required=True)
|
||||||
|
parser.add_argument("--label", default="Private credential")
|
||||||
|
actions = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
actions.add_argument("--store", action="store_true")
|
||||||
|
actions.add_argument("--status", action="store_true")
|
||||||
|
actions.add_argument(
|
||||||
|
"--delete-test-item", action="store_true", help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_metadata(args: argparse.Namespace) -> bool:
|
||||||
|
values = (args.service, args.account, args.label)
|
||||||
|
return all(value and "\x00" not in value for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def native_command() -> Iterator[tuple[list[str], Path | None] | None]:
|
||||||
|
test_helper = os.environ.get("PRIVATE_PASSWORD_STORAGE_TEST_HELPER")
|
||||||
|
if test_helper:
|
||||||
|
yield ([test_helper], None)
|
||||||
|
return
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
|
discovery = subprocess.run(
|
||||||
|
["/usr/bin/xcrun", "--find", "swiftc"],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if discovery.returncode != 0:
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
if not discovery.stdout.strip():
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
|
||||||
|
target = f"{platform.machine()}-apple-macosx12.0"
|
||||||
|
with tempfile.TemporaryDirectory(prefix="private-password-storage-") as temp:
|
||||||
|
app = Path(temp) / "Private Password Storage.app"
|
||||||
|
macos = app / "Contents" / "MacOS"
|
||||||
|
macos.mkdir(parents=True)
|
||||||
|
executable = macos / "private-password-storage-native"
|
||||||
|
info = {
|
||||||
|
"CFBundleDisplayName": "Private Password Storage",
|
||||||
|
"CFBundleExecutable": executable.name,
|
||||||
|
"CFBundleIdentifier": "dev.codex.private-password-storage",
|
||||||
|
"CFBundleName": "Private Password Storage",
|
||||||
|
"CFBundlePackageType": "APPL",
|
||||||
|
"CFBundleVersion": "1",
|
||||||
|
"CFBundleShortVersionString": "1.0",
|
||||||
|
"LSMinimumSystemVersion": "12.0",
|
||||||
|
"NSHighResolutionCapable": True,
|
||||||
|
}
|
||||||
|
with (app / "Contents" / "Info.plist").open("wb") as handle:
|
||||||
|
plistlib.dump(info, handle)
|
||||||
|
|
||||||
|
compilation = subprocess.run(
|
||||||
|
[
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"swiftc",
|
||||||
|
"-target",
|
||||||
|
target,
|
||||||
|
str(Path(__file__).with_suffix(".swift")),
|
||||||
|
"-o",
|
||||||
|
str(executable),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if compilation.returncode != 0:
|
||||||
|
yield None
|
||||||
|
return
|
||||||
|
yield ([str(executable)], app)
|
||||||
|
|
||||||
|
|
||||||
|
def action_args(args: argparse.Namespace) -> list[str]:
|
||||||
|
if args.store:
|
||||||
|
action = "--store"
|
||||||
|
elif args.status:
|
||||||
|
action = "--status"
|
||||||
|
else:
|
||||||
|
action = "--delete-test-item"
|
||||||
|
return [
|
||||||
|
"--service",
|
||||||
|
args.service,
|
||||||
|
"--account",
|
||||||
|
args.account,
|
||||||
|
"--label",
|
||||||
|
args.label,
|
||||||
|
action,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
args = parse_args()
|
||||||
|
except (ValueError, SystemExit):
|
||||||
|
sys.stdout.write("error\n")
|
||||||
|
return 64
|
||||||
|
|
||||||
|
if not validate_metadata(args):
|
||||||
|
sys.stdout.write("error\n")
|
||||||
|
return 64
|
||||||
|
|
||||||
|
with native_command() as native:
|
||||||
|
if native is None:
|
||||||
|
sys.stdout.write("error\n")
|
||||||
|
return 69
|
||||||
|
command, app = native
|
||||||
|
try:
|
||||||
|
if args.store and app is not None:
|
||||||
|
result_file = app.parent / "store-status"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
"/usr/bin/open",
|
||||||
|
"-W",
|
||||||
|
"-n",
|
||||||
|
str(app),
|
||||||
|
"--args",
|
||||||
|
*action_args(args),
|
||||||
|
"--result-file",
|
||||||
|
str(result_file),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
output = (
|
||||||
|
result_file.read_text(encoding="utf-8").strip()
|
||||||
|
if result_file.is_file()
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
completed = subprocess.run(
|
||||||
|
[*command, *action_args(args)],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
output = completed.stdout.strip()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.stdout.write("cancelled\n")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
allowed = {
|
||||||
|
"stored": {"stored", "cancelled"},
|
||||||
|
"status": {"present", "absent"},
|
||||||
|
"delete": {"absent"},
|
||||||
|
}
|
||||||
|
expected = "stored" if args.store else "status" if args.status else "delete"
|
||||||
|
if completed.returncode not in (0, 1) or output not in allowed[expected]:
|
||||||
|
sys.stdout.write("error\n")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
sys.stdout.write(f"{output}\n")
|
||||||
|
if expected == "stored" and output != "stored":
|
||||||
|
return 1
|
||||||
|
return completed.returncode
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
private enum Action {
|
||||||
|
case store
|
||||||
|
case status
|
||||||
|
case deleteTestItem
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Options {
|
||||||
|
let service: String
|
||||||
|
let account: String
|
||||||
|
let label: String
|
||||||
|
let action: Action
|
||||||
|
let resultFile: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum StoreResult {
|
||||||
|
case stored
|
||||||
|
case cancelled
|
||||||
|
case failed
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PresenceResult {
|
||||||
|
case present
|
||||||
|
case absent
|
||||||
|
case failed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseOptions() -> Options? {
|
||||||
|
var service: String?
|
||||||
|
var account: String?
|
||||||
|
var label = "Private credential"
|
||||||
|
var action: Action?
|
||||||
|
var resultFile: String?
|
||||||
|
var index = 1
|
||||||
|
let arguments = CommandLine.arguments
|
||||||
|
|
||||||
|
while index < arguments.count {
|
||||||
|
let argument = arguments[index]
|
||||||
|
switch argument {
|
||||||
|
case "--service", "--account", "--label", "--result-file":
|
||||||
|
guard index + 1 < arguments.count else { return nil }
|
||||||
|
let value = arguments[index + 1]
|
||||||
|
guard !value.isEmpty, !value.contains("\0") else { return nil }
|
||||||
|
if argument == "--service" {
|
||||||
|
service = value
|
||||||
|
} else if argument == "--account" {
|
||||||
|
account = value
|
||||||
|
} else if argument == "--label" {
|
||||||
|
label = value
|
||||||
|
} else {
|
||||||
|
resultFile = value
|
||||||
|
}
|
||||||
|
index += 2
|
||||||
|
case "--store", "--status", "--delete-test-item":
|
||||||
|
guard action == nil else { return nil }
|
||||||
|
action = argument == "--store" ? .store
|
||||||
|
: argument == "--status" ? .status : .deleteTestItem
|
||||||
|
index += 1
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let service, let account, let action, !label.isEmpty else { return nil }
|
||||||
|
return Options(
|
||||||
|
service: service,
|
||||||
|
account: account,
|
||||||
|
label: label,
|
||||||
|
action: action,
|
||||||
|
resultFile: resultFile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deviceLocalQuery(service: String, account: String) -> [CFString: Any] {
|
||||||
|
[
|
||||||
|
kSecClass: kSecClassGenericPassword,
|
||||||
|
kSecAttrService: service,
|
||||||
|
kSecAttrAccount: account,
|
||||||
|
kSecAttrSynchronizable: kCFBooleanFalse as Any,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func storeInKeychain(
|
||||||
|
service: String,
|
||||||
|
account: String,
|
||||||
|
label: String,
|
||||||
|
secretData: Data
|
||||||
|
) -> Bool {
|
||||||
|
let query = deviceLocalQuery(service: service, account: account)
|
||||||
|
var item = query
|
||||||
|
item[kSecAttrLabel] = label
|
||||||
|
item[kSecAttrAccessible] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||||
|
item[kSecValueData] = secretData
|
||||||
|
|
||||||
|
let addStatus = SecItemAdd(item as CFDictionary, nil)
|
||||||
|
if addStatus == errSecSuccess {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard addStatus == errSecDuplicateItem else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let updates: [CFString: Any] = [
|
||||||
|
kSecAttrLabel: label,
|
||||||
|
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
kSecValueData: secretData,
|
||||||
|
]
|
||||||
|
return SecItemUpdate(query as CFDictionary, updates as CFDictionary) == errSecSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
private func keychainPresence(service: String, account: String) -> PresenceResult {
|
||||||
|
var query = deviceLocalQuery(service: service, account: account)
|
||||||
|
query[kSecMatchLimit] = kSecMatchLimitOne
|
||||||
|
query[kSecReturnData] = kCFBooleanFalse
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||||
|
if status == errSecSuccess {
|
||||||
|
return .present
|
||||||
|
}
|
||||||
|
if status == errSecItemNotFound {
|
||||||
|
return .absent
|
||||||
|
}
|
||||||
|
return .failed
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteExactCanaryItem(service: String, account: String) -> Bool {
|
||||||
|
guard service.hasPrefix("dev.codex.private-password-storage.canary."),
|
||||||
|
account.hasPrefix("canary-") else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let query = deviceLocalQuery(service: service, account: account)
|
||||||
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
|
return status == errSecSuccess || status == errSecItemNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private final class SecurePromptController: NSObject, NSWindowDelegate {
|
||||||
|
private let service: String
|
||||||
|
private let account: String
|
||||||
|
private let itemLabel: String
|
||||||
|
private let window: NSWindow
|
||||||
|
private let secureField = NSSecureTextField()
|
||||||
|
private let validationLabel = NSTextField(labelWithString: "")
|
||||||
|
private var result: StoreResult?
|
||||||
|
|
||||||
|
init(service: String, account: String, label: String) {
|
||||||
|
self.service = service
|
||||||
|
self.account = account
|
||||||
|
self.itemLabel = label
|
||||||
|
self.window = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 440, height: 190),
|
||||||
|
styleMask: [.titled, .closable],
|
||||||
|
backing: .buffered,
|
||||||
|
defer: false
|
||||||
|
)
|
||||||
|
super.init()
|
||||||
|
configureWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureWindow() {
|
||||||
|
window.title = "Store Private Credential"
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
window.delegate = self
|
||||||
|
window.center()
|
||||||
|
|
||||||
|
let content = NSView()
|
||||||
|
window.contentView = content
|
||||||
|
|
||||||
|
let title = NSTextField(labelWithString: itemLabel)
|
||||||
|
title.font = NSFont.systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
title.lineBreakMode = .byTruncatingTail
|
||||||
|
secureField.placeholderString = "Password"
|
||||||
|
secureField.font = NSFont.systemFont(ofSize: 14)
|
||||||
|
|
||||||
|
validationLabel.stringValue = "Password cannot be empty."
|
||||||
|
validationLabel.textColor = .systemRed
|
||||||
|
validationLabel.isHidden = true
|
||||||
|
|
||||||
|
let cancelButton = NSButton(
|
||||||
|
title: "Cancel", target: self, action: #selector(cancelPressed)
|
||||||
|
)
|
||||||
|
cancelButton.keyEquivalent = "\u{1b}"
|
||||||
|
let storeButton = NSButton(
|
||||||
|
title: "Store", target: self, action: #selector(storePressed)
|
||||||
|
)
|
||||||
|
storeButton.keyEquivalent = "\r"
|
||||||
|
|
||||||
|
let pasteMenu = NSMenu()
|
||||||
|
let pasteItem = NSMenuItem(
|
||||||
|
title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: ""
|
||||||
|
)
|
||||||
|
pasteItem.target = nil
|
||||||
|
pasteMenu.addItem(pasteItem)
|
||||||
|
secureField.menu = pasteMenu
|
||||||
|
|
||||||
|
[title, secureField, validationLabel, cancelButton, storeButton].forEach {
|
||||||
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
content.addSubview($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
title.topAnchor.constraint(equalTo: content.topAnchor, constant: 24),
|
||||||
|
title.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 24),
|
||||||
|
title.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24),
|
||||||
|
secureField.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16),
|
||||||
|
secureField.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
||||||
|
secureField.trailingAnchor.constraint(equalTo: title.trailingAnchor),
|
||||||
|
secureField.heightAnchor.constraint(equalToConstant: 28),
|
||||||
|
validationLabel.topAnchor.constraint(equalTo: secureField.bottomAnchor, constant: 6),
|
||||||
|
validationLabel.leadingAnchor.constraint(equalTo: secureField.leadingAnchor),
|
||||||
|
storeButton.trailingAnchor.constraint(equalTo: secureField.trailingAnchor),
|
||||||
|
storeButton.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -20),
|
||||||
|
cancelButton.trailingAnchor.constraint(equalTo: storeButton.leadingAnchor, constant: -10),
|
||||||
|
cancelButton.centerYAnchor.constraint(equalTo: storeButton.centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func installPasteMenu() {
|
||||||
|
let mainMenu = NSMenu()
|
||||||
|
let editMenuItem = NSMenuItem()
|
||||||
|
let editMenu = NSMenu(title: "Edit")
|
||||||
|
let pasteItem = NSMenuItem(
|
||||||
|
title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v"
|
||||||
|
)
|
||||||
|
pasteItem.keyEquivalentModifierMask = [.command]
|
||||||
|
pasteItem.target = nil
|
||||||
|
editMenu.addItem(pasteItem)
|
||||||
|
editMenuItem.submenu = editMenu
|
||||||
|
mainMenu.addItem(editMenuItem)
|
||||||
|
NSApp.mainMenu = mainMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() -> StoreResult {
|
||||||
|
installPasteMenu()
|
||||||
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.makeFirstResponder(secureField)
|
||||||
|
NSApp.run()
|
||||||
|
return result ?? .cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func storePressed() {
|
||||||
|
guard !secureField.stringValue.isEmpty else {
|
||||||
|
validationLabel.stringValue = "Password cannot be empty."
|
||||||
|
validationLabel.isHidden = false
|
||||||
|
NSSound.beep()
|
||||||
|
window.makeFirstResponder(secureField)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationLabel.isHidden = true
|
||||||
|
var secretData = Data(secureField.stringValue.utf8)
|
||||||
|
let stored = storeInKeychain(
|
||||||
|
service: service,
|
||||||
|
account: account,
|
||||||
|
label: itemLabel,
|
||||||
|
secretData: secretData
|
||||||
|
)
|
||||||
|
secureField.stringValue = ""
|
||||||
|
secretData.resetBytes(in: 0..<secretData.count)
|
||||||
|
guard stored else {
|
||||||
|
validationLabel.stringValue = "The credential could not be stored."
|
||||||
|
validationLabel.isHidden = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = .stored
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func cancelPressed() {
|
||||||
|
secureField.stringValue = ""
|
||||||
|
result = .cancelled
|
||||||
|
window.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowWillClose(_ notification: Notification) {
|
||||||
|
secureField.stringValue = ""
|
||||||
|
if result == nil {
|
||||||
|
result = .cancelled
|
||||||
|
}
|
||||||
|
NSApp.stop(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func runSecurePrompt(options: Options) -> StoreResult {
|
||||||
|
let application = NSApplication.shared
|
||||||
|
let controller = SecurePromptController(
|
||||||
|
service: options.service,
|
||||||
|
account: options.account,
|
||||||
|
label: options.label
|
||||||
|
)
|
||||||
|
_ = application
|
||||||
|
return controller.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emit(_ value: String, code: Int32, resultFile: String? = nil) -> Never {
|
||||||
|
let data = Data((value + "\n").utf8)
|
||||||
|
if let resultFile {
|
||||||
|
do {
|
||||||
|
try data.write(to: URL(fileURLWithPath: resultFile), options: .atomic)
|
||||||
|
} catch {
|
||||||
|
exit(74)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FileHandle.standardOutput.write(data)
|
||||||
|
}
|
||||||
|
exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let options = parseOptions() else {
|
||||||
|
emit("error", code: 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch options.action {
|
||||||
|
case .status:
|
||||||
|
switch keychainPresence(service: options.service, account: options.account) {
|
||||||
|
case .present:
|
||||||
|
emit("present", code: 0, resultFile: options.resultFile)
|
||||||
|
case .absent:
|
||||||
|
emit("absent", code: 1, resultFile: options.resultFile)
|
||||||
|
case .failed:
|
||||||
|
emit("error", code: 1, resultFile: options.resultFile)
|
||||||
|
}
|
||||||
|
case .store:
|
||||||
|
let storeResult = MainActor.assumeIsolated {
|
||||||
|
runSecurePrompt(options: options)
|
||||||
|
}
|
||||||
|
switch storeResult {
|
||||||
|
case .stored:
|
||||||
|
emit("stored", code: 0, resultFile: options.resultFile)
|
||||||
|
case .cancelled:
|
||||||
|
emit("cancelled", code: 1, resultFile: options.resultFile)
|
||||||
|
case .failed:
|
||||||
|
emit("error", code: 1, resultFile: options.resultFile)
|
||||||
|
}
|
||||||
|
case .deleteTestItem:
|
||||||
|
guard deleteExactCanaryItem(service: options.service, account: options.account) else {
|
||||||
|
emit("error", code: 1, resultFile: options.resultFile)
|
||||||
|
}
|
||||||
|
emit("absent", code: 0, resultFile: options.resultFile)
|
||||||
|
}
|
||||||
178
tests/test_private_password_storage_skill.py
Normal file
178
tests/test_private_password_storage_skill.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SKILL = ROOT / ".agents" / "skills" / "private-password-storage"
|
||||||
|
LAUNCHER = SKILL / "scripts" / "private-password-storage"
|
||||||
|
PYTHON_HELPER = SKILL / "scripts" / "private_password_storage.py"
|
||||||
|
SWIFT_HELPER = SKILL / "scripts" / "private_password_storage.swift"
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_helper(tmp_path: Path) -> tuple[Path, Path]:
|
||||||
|
helper = tmp_path / "fake_keychain_helper.py"
|
||||||
|
capture = tmp_path / "captured_args.json"
|
||||||
|
helper.write_text(
|
||||||
|
"""#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with open(os.environ["PRIVATE_PASSWORD_STORAGE_TEST_CAPTURE"], "w", encoding="utf-8") as handle:
|
||||||
|
json.dump(sys.argv[1:], handle)
|
||||||
|
|
||||||
|
if "--store" in sys.argv:
|
||||||
|
print("stored")
|
||||||
|
elif "--status" in sys.argv:
|
||||||
|
print("present")
|
||||||
|
elif "--delete-test-item" in sys.argv:
|
||||||
|
print("absent")
|
||||||
|
else:
|
||||||
|
print("error")
|
||||||
|
raise SystemExit(64)
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
helper.chmod(0o700)
|
||||||
|
return helper, capture
|
||||||
|
|
||||||
|
|
||||||
|
def _run_fake(tmp_path: Path, action: str) -> tuple[subprocess.CompletedProcess[str], list[str]]:
|
||||||
|
helper, capture = _fake_helper(tmp_path)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PRIVATE_PASSWORD_STORAGE_TEST_HELPER"] = str(helper)
|
||||||
|
env["PRIVATE_PASSWORD_STORAGE_TEST_CAPTURE"] = str(capture)
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
str(LAUNCHER),
|
||||||
|
"--service",
|
||||||
|
"dev.codex.private-password-storage.canary.ci",
|
||||||
|
"--account",
|
||||||
|
"canary-ci",
|
||||||
|
"--label",
|
||||||
|
"CI fake credential",
|
||||||
|
action,
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
return completed, json.loads(capture.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("action", "expected", "returncode"),
|
||||||
|
[
|
||||||
|
("--store", "stored\n", 0),
|
||||||
|
("--status", "present\n", 0),
|
||||||
|
("--delete-test-item", "absent\n", 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_launcher_uses_only_metadata_and_action(
|
||||||
|
tmp_path: Path, action: str, expected: str, returncode: int
|
||||||
|
) -> None:
|
||||||
|
completed, captured = _run_fake(tmp_path, action)
|
||||||
|
|
||||||
|
assert completed.returncode == returncode
|
||||||
|
assert completed.stdout == expected
|
||||||
|
assert completed.stderr == ""
|
||||||
|
assert captured == [
|
||||||
|
"--service",
|
||||||
|
"dev.codex.private-password-storage.canary.ci",
|
||||||
|
"--account",
|
||||||
|
"canary-ci",
|
||||||
|
"--label",
|
||||||
|
"CI fake credential",
|
||||||
|
action,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scripts_ban_unsafe_secret_paths_and_retrieval() -> None:
|
||||||
|
implementation = "\n".join(
|
||||||
|
path.read_text(encoding="utf-8")
|
||||||
|
for path in (LAUNCHER, PYTHON_HELPER, SWIFT_HELPER)
|
||||||
|
)
|
||||||
|
banned = (
|
||||||
|
"AppleScript",
|
||||||
|
"osascript",
|
||||||
|
"System Events",
|
||||||
|
"NSPasteboard",
|
||||||
|
"pbcopy",
|
||||||
|
"security add-generic-password",
|
||||||
|
"--get",
|
||||||
|
"--read-secret",
|
||||||
|
"--retrieve",
|
||||||
|
"kSecReturnData] = kCFBooleanTrue",
|
||||||
|
"kSecReturnAttributes",
|
||||||
|
"print(secret",
|
||||||
|
"print(password",
|
||||||
|
)
|
||||||
|
for token in banned:
|
||||||
|
assert token not in implementation
|
||||||
|
|
||||||
|
|
||||||
|
def test_swift_enforces_device_local_keychain_and_secure_popup() -> None:
|
||||||
|
source = SWIFT_HELPER.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for required in (
|
||||||
|
"NSSecureTextField",
|
||||||
|
'title: "Paste"',
|
||||||
|
"keyEquivalentModifierMask = [.command]",
|
||||||
|
"guard !secureField.stringValue.isEmpty",
|
||||||
|
"kSecAttrAccessibleWhenUnlockedThisDeviceOnly",
|
||||||
|
"kSecAttrSynchronizable: kCFBooleanFalse",
|
||||||
|
"SecItemAdd",
|
||||||
|
"SecItemUpdate",
|
||||||
|
"SecItemCopyMatching",
|
||||||
|
"SecItemDelete",
|
||||||
|
'service.hasPrefix("dev.codex.private-password-storage.canary.")',
|
||||||
|
'account.hasPrefix("canary-")',
|
||||||
|
):
|
||||||
|
assert required in source
|
||||||
|
|
||||||
|
store_case = source.split("case .store:", 1)[1].split(
|
||||||
|
"case .deleteTestItem:", 1
|
||||||
|
)[0]
|
||||||
|
assert "runSecurePrompt" in store_case
|
||||||
|
assert "runSecurePrompt" not in source.split("case .status:", 1)[1].split(
|
||||||
|
"case .store:", 1
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_launcher_builds_an_ephemeral_identifiable_native_app() -> None:
|
||||||
|
source = PYTHON_HELPER.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert 'TemporaryDirectory(prefix="private-password-storage-")' in source
|
||||||
|
assert '"CFBundleIdentifier": "dev.codex.private-password-storage"' in source
|
||||||
|
assert '"CFBundlePackageType": "APPL"' in source
|
||||||
|
assert '"NSHighResolutionCapable": True' in source
|
||||||
|
assert '"/usr/bin/open"' in source
|
||||||
|
assert 'result_file = app.parent / "store-status"' in source
|
||||||
|
assert "secretData.write" not in SWIFT_HELPER.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS AppKit SDK")
|
||||||
|
def test_embedded_swift_helper_typechecks() -> None:
|
||||||
|
target = f"{platform.machine()}-apple-macosx12.0"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
"swiftc",
|
||||||
|
"-target",
|
||||||
|
target,
|
||||||
|
"-typecheck",
|
||||||
|
str(SWIFT_HELPER),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert completed.returncode == 0, completed.stderr
|
||||||
Loading…
Reference in a new issue