using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Client; /// /// Scrubs exact secret substrings out of diagnostic text before it leaves the client on an /// exception path. MXAccess can echo a submitted credential or secured value back inside a /// failure diagnostic (protocol message, MXSTATUS_PROXY diagnostic text, HRESULT description); /// this helper replaces any such verbatim occurrence with <redacted> so the raw /// request payload never reaches a caught exception's message. The marker matches the Go, Rust, /// and Java clients. /// internal static class MxGatewaySecretRedaction { private const string Marker = ""; /// /// Replaces every usable secret in with the redaction marker /// (ordinal comparison). Returns the message unchanged when it is null or empty, or when no /// usable secret is supplied. A secret that is null, empty, or whitespace-only is ignored so /// it cannot over-redact ordinary separator characters in the message. /// /// The diagnostic message to scrub. /// The secret values to remove from the message. /// The scrubbed message. internal static string Redact(string message, params string?[] secrets) { if (string.IsNullOrEmpty(message) || secrets is null) { return message; } string result = message; foreach (string? secret in secrets) { if (!string.IsNullOrWhiteSpace(secret)) { result = result.Replace(secret, Marker, StringComparison.Ordinal); } } return result; } /// /// Returns a scrubbed clone of : the protocol-status message, the /// reply-level diagnostic message, and each MXSTATUS_PROXY diagnostic text have every verbatim /// secret replaced with the redaction marker. The original is left untouched. MXAccess can echo /// a submitted credential into any of these fields, so a redacted exception must carry the /// scrubbed reply rather than the secret-bearing original. /// /// The reply to clone and scrub. /// The secret values to remove. /// A scrubbed clone of the reply. internal static MxCommandReply RedactReply(MxCommandReply reply, params string?[] secrets) { ArgumentNullException.ThrowIfNull(reply); MxCommandReply clone = reply.Clone(); if (clone.ProtocolStatus is not null) { clone.ProtocolStatus.Message = Redact(clone.ProtocolStatus.Message, secrets); } clone.DiagnosticMessage = Redact(clone.DiagnosticMessage, secrets); foreach (MxStatusProxy status in clone.Statuses) { status.DiagnosticText = Redact(status.DiagnosticText, secrets); } return clone; } /// /// Returns a scrubbed clone of (its message with every verbatim /// secret removed), or when the input is null. /// /// The protocol status to clone and scrub. /// The secret values to remove. /// A scrubbed clone, or . internal static ProtocolStatus? RedactStatus(ProtocolStatus? status, params string?[] secrets) { if (status is null) { return null; } ProtocolStatus clone = status.Clone(); clone.Message = Redact(clone.Message, secrets); return clone; } /// /// Returns a list of scrubbed clones of — each MXSTATUS_PROXY's /// diagnostic text has every verbatim secret removed. The originals are left untouched. /// /// The statuses to clone and scrub. /// The secret values to remove. /// A list of scrubbed clones. internal static IReadOnlyList RedactStatuses( IReadOnlyList statuses, params string?[] secrets) { if (statuses is null || statuses.Count is 0) { return statuses ?? []; } MxStatusProxy[] result = new MxStatusProxy[statuses.Count]; for (int i = 0; i < statuses.Count; i++) { MxStatusProxy clone = statuses[i].Clone(); clone.DiagnosticText = Redact(clone.DiagnosticText, secrets); result[i] = clone; } return result; } /// /// Returns an exception equivalent to but with any verbatim secret /// scrubbed from its message. When nothing changes, the original exception is returned /// unchanged; otherwise a new exception of the same concrete runtime type is built and the /// original reply/status context is preserved. The secret-bearing original is deliberately /// not chained as the inner exception — doing so would let its unredacted message /// re-surface through (which logging frameworks call). The /// original's own inner cause (a transport error, never the request payload) is carried /// forward instead. /// /// The exception to redact. /// The secret values to remove from the message. /// The redacted exception, or the original when no change was needed. internal static MxGatewayException Redacted(MxGatewayException ex, params string?[] secrets) { ArgumentNullException.ThrowIfNull(ex); string redacted = Redact(ex.Message, secrets); bool messageChanged = !string.Equals(redacted, ex.Message, StringComparison.Ordinal); Exception? cause = ex.InnerException; // MxAccessException derives its structured fields from the raw reply, so scrubbing must // clone and redact that reply — the message alone changing is not enough, because the reply // can carry the echoed secret even when the message does not. if (ex is MxAccessException access) { if (!messageChanged && !ReplyContainsSecret(access.Reply, secrets)) { return ex; } return new MxAccessException(redacted, RedactReply(access.Reply, secrets), cause); } // Other subtypes carry the secret through ProtocolStatus.Message and Statuses[].DiagnosticText. if (!messageChanged && !ContainsSecret(ex.ProtocolStatus?.Message, secrets) && !StatusesContainSecret(ex.Statuses, secrets)) { return ex; } ProtocolStatus? status = RedactStatus(ex.ProtocolStatus, secrets); IReadOnlyList statuses = RedactStatuses(ex.Statuses, secrets); return ex switch { MxGatewaySessionException => new MxGatewaySessionException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), MxGatewayWorkerException => new MxGatewayWorkerException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), MxGatewayAuthenticationException => new MxGatewayAuthenticationException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), MxGatewayAuthorizationException => new MxGatewayAuthorizationException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), MxGatewayMalformedReplyException => new MxGatewayMalformedReplyException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), MxGatewayCommandException => new MxGatewayCommandException( redacted, ex.SessionId, ex.CorrelationId, status, ex.HResultCode, statuses, cause), _ => new MxGatewayException(redacted, cause), }; } private static bool ContainsSecret(string? text, string?[] secrets) { if (string.IsNullOrEmpty(text) || secrets is null) { return false; } foreach (string? secret in secrets) { if (!string.IsNullOrWhiteSpace(secret) && text.Contains(secret, StringComparison.Ordinal)) { return true; } } return false; } private static bool StatusesContainSecret(IReadOnlyList statuses, string?[] secrets) { if (statuses is null) { return false; } foreach (MxStatusProxy status in statuses) { if (ContainsSecret(status.DiagnosticText, secrets)) { return true; } } return false; } private static bool ReplyContainsSecret(MxCommandReply reply, string?[] secrets) { if (reply is null) { return false; } return ContainsSecret(reply.ProtocolStatus?.Message, secrets) || ContainsSecret(reply.DiagnosticMessage, secrets) || StatusesContainSecret(reply.Statuses, secrets); } }