Files
Shutong Wu e728c35adb feat(asset-gen): SecureKeyStore — theft-resistant at-rest key storage (Phase 1)
OS secure store per platform: macOS Keychain (/usr/bin/security), Windows Credential
Manager (advapi32 P/Invoke), Linux secret-tool; AES-256-CBC+HMAC encrypt-then-MAC
fallback (CI-safe, master secret + machine id via PBKDF2, ciphertext under user
app-data, never in repo). Env override (MCPFORUNITY_<P>_API_KEY, read-only) layered on
top; SecretRedactor scrubs auth tokens. Keys never touch EditorPrefs/bridge/logs/git.
EditMode tests for fallback round-trip, encryption-at-rest, env override, redaction.
Compiles clean on Unity 2021.3.45f2 (floor).

Claude-Session: https://claude.ai/code/session_01Tjpb5gYgUe2AUJuRdXr7Lv
2026-06-28 19:25:20 -07:00

68 lines
2.4 KiB
C#

using System;
using System.Diagnostics;
namespace MCPForUnity.Editor.Security
{
/// <summary>
/// macOS Keychain-backed key store via /usr/bin/security generic passwords.
/// Service = MCPForUnity.AssetGen, account = provider id.
/// </summary>
internal sealed class MacKeychainKeyStore : ISecureKeyStore
{
private const string Security = "/usr/bin/security";
private const string Service = SecureKeyStoreConstants.ServiceName;
public bool Has(string providerId) => TryGet(providerId, out _);
public bool TryGet(string providerId, out string apiKey)
{
apiKey = null;
if (string.IsNullOrEmpty(providerId)) return false;
(int code, string stdout, _) = Run("find-generic-password", "-s", Service, "-a", providerId, "-w");
if (code != 0) return false;
apiKey = (stdout ?? string.Empty).TrimEnd('\n', '\r');
return !string.IsNullOrEmpty(apiKey);
}
public void Set(string providerId, string apiKey)
{
if (string.IsNullOrEmpty(providerId)) return;
if (string.IsNullOrEmpty(apiKey)) { Delete(providerId); return; }
// -U overwrites an existing item for this service/account.
Run("add-generic-password", "-U", "-s", Service, "-a", providerId, "-w", apiKey);
}
public void Delete(string providerId)
{
if (string.IsNullOrEmpty(providerId)) return;
Run("delete-generic-password", "-s", Service, "-a", providerId);
}
private static (int code, string stdout, string stderr) Run(params string[] args)
{
try
{
var psi = new ProcessStartInfo(Security)
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (string a in args) psi.ArgumentList.Add(a);
using (var p = Process.Start(psi))
{
string outp = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
p.WaitForExit(5000);
return (p.ExitCode, outp, err);
}
}
catch (Exception e)
{
return (-1, null, e.Message);
}
}
}
}