using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.Json; namespace Dada.Supervisor; internal enum ChildRole { Api, Worker, } internal static class CredentialCatalog { internal const string ApiResend = "Dada/P0A/api/resend"; internal const string ApiAmap = "Dada/P0A/api/amap"; internal const string WorkerAiGateway = "Dada/P0A/worker/ai-gateway"; internal const string AdminPepper = "Dada/P0A/admin/pepper"; internal static IReadOnlyList RequiredFor(ChildRole role) => role switch { ChildRole.Api => [ApiResend, ApiAmap, AdminPepper], ChildRole.Worker => [WorkerAiGateway], _ => throw new ArgumentOutOfRangeException(nameof(role)), }; } internal interface ICredentialStore { string? Read(string target); void Write(string target, string value); void Delete(string target); bool IsConfigured(string target); } internal sealed class MissingCredentialException(string target) : InvalidOperationException($"Required credential is not configured: {target}"); internal sealed record CredentialProcessResult(int ExitCode, string StandardOutput, string StandardError, bool SensitiveOutputDetected); internal static class CredentialProcessLauncher { internal static async Task RunToCompletionAsync( ProcessStartInfo startInfo, ChildRole role, ICredentialStore store, CancellationToken cancellationToken = default) { var credentials = new Dictionary(StringComparer.Ordinal); foreach (var target in CredentialCatalog.RequiredFor(role)) { credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target); } startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardInput = true; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardError = true; using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start credential child process."); var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); var payload = JsonSerializer.SerializeToUtf8Bytes(credentials); try { await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken); await process.StandardInput.BaseStream.FlushAsync(cancellationToken); process.StandardInput.Close(); await process.WaitForExitAsync(cancellationToken); var output = await outputTask; var error = await errorTask; var sensitive = credentials.Values.Where(value => value.Length > 0).Any(value => output.Contains(value, StringComparison.Ordinal) || error.Contains(value, StringComparison.Ordinal)); return sensitive ? new CredentialProcessResult(1, string.Empty, string.Empty, true) : new CredentialProcessResult(process.ExitCode, output, error, false); } catch { if (!process.HasExited) process.Kill(entireProcessTree: true); throw; } finally { Array.Clear(payload); foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty; credentials.Clear(); } } internal static async Task StartAsync( ProcessStartInfo startInfo, ChildRole role, ICredentialStore store, CancellationToken cancellationToken = default) { var credentials = new Dictionary(StringComparer.Ordinal); foreach (var target in CredentialCatalog.RequiredFor(role)) { credentials[target] = store.Read(target) ?? throw new MissingCredentialException(target); } startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardInput = true; var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Unable to start managed child process."); var payload = JsonSerializer.SerializeToUtf8Bytes(credentials); try { await process.StandardInput.BaseStream.WriteAsync(payload, cancellationToken); await process.StandardInput.BaseStream.FlushAsync(cancellationToken); } catch { if (!process.HasExited) process.Kill(entireProcessTree: true); process.Dispose(); throw; } finally { Array.Clear(payload); process.StandardInput.Close(); foreach (var target in credentials.Keys.ToArray()) credentials[target] = string.Empty; credentials.Clear(); } return process; } } internal sealed class WindowsCredentialStore : ICredentialStore { public string? Read(string target) { if (!CredRead(target, CredentialType.Generic, 0, out var pointer)) { var error = Marshal.GetLastWin32Error(); if (error == ErrorNotFound) return null; throw new Win32Exception(error); } try { var credential = Marshal.PtrToStructure(pointer); return credential.CredentialBlobSize == 0 ? string.Empty : Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / sizeof(char)); } finally { CredFree(pointer); } } public void Write(string target, string value) { var bytes = System.Text.Encoding.Unicode.GetBytes(value); if (bytes.Length > MaximumGenericBlobSize) throw new ArgumentOutOfRangeException(nameof(value)); var pointer = Marshal.AllocHGlobal(bytes.Length); try { Marshal.Copy(bytes, 0, pointer, bytes.Length); var credential = new NativeCredential { Type = CredentialType.Generic, TargetName = target, CredentialBlobSize = bytes.Length, CredentialBlob = pointer, Persist = CredentialPersistence.LocalMachine, UserName = Environment.UserName, }; if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error()); } finally { Marshal.Copy(new byte[bytes.Length], 0, pointer, bytes.Length); Marshal.FreeHGlobal(pointer); Array.Clear(bytes); } } public void Delete(string target) { if (CredDelete(target, CredentialType.Generic, 0)) return; var error = Marshal.GetLastWin32Error(); if (error != ErrorNotFound) throw new Win32Exception(error); } public bool IsConfigured(string target) => Read(target) is not null; private const int ErrorNotFound = 1168; private const int MaximumGenericBlobSize = 5 * 512; [DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CredRead(string target, CredentialType type, int flags, out IntPtr credential); [DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CredWrite([In] ref NativeCredential credential, int flags); [DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool CredDelete(string target, CredentialType type, int flags); [DllImport("Advapi32.dll")] private static extern void CredFree(IntPtr buffer); private enum CredentialType : uint { Generic = 1 } private enum CredentialPersistence : uint { LocalMachine = 2 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct NativeCredential { public uint Flags; public CredentialType Type; public string TargetName; public string? Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public int CredentialBlobSize; public IntPtr CredentialBlob; public CredentialPersistence Persist; public int AttributeCount; public IntPtr Attributes; public string? TargetAlias; public string UserName; } }