|
| 1 | +// Package redact implements functions to redact sensitive information from |
| 2 | +// errors. |
| 3 | +// |
| 4 | +// Redacting an error replaces its message with a placeholder while still |
| 5 | +// maintaining wrapped errors: |
| 6 | +// |
| 7 | +// wrapped := errors.New("not found") |
| 8 | +// name := "Alex" |
| 9 | +// err := fmt.Errorf("error getting user %s: %w", name, wrapped) |
| 10 | +// |
| 11 | +// fmt.Println(err) |
| 12 | +// // error getting user Alex: not found |
| 13 | +// |
| 14 | +// fmt.Println(Error(err)) |
| 15 | +// // <redacted *fmt.wrapError>: <redacted *errors.errorString> |
| 16 | +// |
| 17 | +// If an error implements a Redact() string method, then it is said to be |
| 18 | +// redactable. A redactable error defines an alternative message for its |
| 19 | +// redacted form: |
| 20 | +// |
| 21 | +// type userErr struct{ name string } |
| 22 | +// |
| 23 | +// func (e *userErr) Error() string { |
| 24 | +// return fmt.Sprintf("user %s not found", e.name) |
| 25 | +// } |
| 26 | +// |
| 27 | +// func (e *userErr) Redact() string { |
| 28 | +// return fmt.Sprintf("user %x not found", sha256.Sum256([]byte(e.name))) |
| 29 | +// } |
| 30 | +// |
| 31 | +// func main() { |
| 32 | +// err := &userErr{name: "Alex"} |
| 33 | +// fmt.Println(err) |
| 34 | +// // user Alex not found |
| 35 | +// |
| 36 | +// fmt.Println(Error(err)) |
| 37 | +// // user db74c940d447e877d119df613edd2700c4a84cd1cf08beb7cbc319bcfaeab97a not found |
| 38 | +// } |
| 39 | +// |
| 40 | +// The [Errorf] function creates redactable errors that retain their literal |
| 41 | +// format text, but redact any arguments. The format string spec is identical |
| 42 | +// to that of [fmt.Errorf]. Calling [Safe] on an [Errorf] argument will include |
| 43 | +// it in the redacted message. |
| 44 | +// |
| 45 | +// name := "Alex" |
| 46 | +// id := 5 |
| 47 | +// err := Errorf("error getting user %s with ID %d", name, Safe(id)) |
| 48 | +// |
| 49 | +// fmt.Println(err) |
| 50 | +// // error getting user Alex with ID 5 |
| 51 | +// |
| 52 | +// fmt.Println(Error(err)) |
| 53 | +// // error getting user <redacted string> with ID 5 |
| 54 | +// |
| 55 | +//nolint:errorlint |
| 56 | +package redact |
| 57 | + |
| 58 | +import ( |
| 59 | + "errors" |
| 60 | + "fmt" |
| 61 | + "runtime" |
| 62 | +) |
| 63 | + |
| 64 | +// Error returns a redacted error that wraps err. If err has a Redact() string |
| 65 | +// method, then Error uses it for the redacted error message. Otherwise, Error |
| 66 | +// recursively redacts each wrapped error, joining them with ": " to create the |
| 67 | +// final error message. If it encounters an error that has a Redact() method, |
| 68 | +// then it appends the result of Redact() to the message and stops unwrapping. |
| 69 | +func Error(err error) error { |
| 70 | + if err == nil { |
| 71 | + return nil |
| 72 | + } |
| 73 | + |
| 74 | + switch t := err.(type) { |
| 75 | + case *redactedError: |
| 76 | + // Don't redact an already redacted error, otherwise its redacted message |
| 77 | + // will be replaced with a placeholder. |
| 78 | + return err |
| 79 | + case redactor: |
| 80 | + return &redactedError{ |
| 81 | + msg: t.Redact(), |
| 82 | + wrapped: err, |
| 83 | + } |
| 84 | + default: |
| 85 | + msg := placeholder(err) |
| 86 | + wrapped := err |
| 87 | + for { |
| 88 | + wrapped = errors.Unwrap(wrapped) |
| 89 | + if wrapped == nil { |
| 90 | + break |
| 91 | + } |
| 92 | + if redactor, ok := wrapped.(redactor); ok { |
| 93 | + msg += ": " + redactor.Redact() |
| 94 | + break |
| 95 | + } |
| 96 | + msg += ": " + placeholder(wrapped) |
| 97 | + } |
| 98 | + return &redactedError{ |
| 99 | + msg: msg, |
| 100 | + wrapped: err, |
| 101 | + } |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +// Errorf creates a redactable error that has an error string identical to that |
| 106 | +// of a [fmt.Errorf] error. Calling [Redact] on the result will redact all |
| 107 | +// format arguments from the error message instead of redacting the entire |
| 108 | +// string. |
| 109 | +// |
| 110 | +// When redacting the error string, Errorf replaces arguments that implement a |
| 111 | +// Redact() string method with the result of that method. To include an |
| 112 | +// argument as-is in the redacted error, first call [Safe]. For example: |
| 113 | +// |
| 114 | +// username := "bob" |
| 115 | +// Errorf("cannot find user %s", username).Error() |
| 116 | +// // cannot find user <redacted string> |
| 117 | +// |
| 118 | +// Errorf("cannot find user %s", Safe(username)).Error() |
| 119 | +// // cannot find user bob |
| 120 | +func Errorf(format string, a ...any) error { |
| 121 | + // Capture a stack trace. |
| 122 | + safeErr := &safeError{ |
| 123 | + callers: make([]uintptr, 32), |
| 124 | + } |
| 125 | + n := runtime.Callers(2, safeErr.callers) |
| 126 | + safeErr.callers = safeErr.callers[:n] |
| 127 | + |
| 128 | + // Create the "normal" unredacted error. We need to remove the safe wrapper |
| 129 | + // from any args so that fmt.Errorf can detect and format their type |
| 130 | + // correctly. |
| 131 | + args := make([]any, len(a)) |
| 132 | + for i := range a { |
| 133 | + if safe, ok := a[i].(safe); ok { |
| 134 | + args[i] = safe.a |
| 135 | + } else { |
| 136 | + args[i] = a[i] |
| 137 | + } |
| 138 | + } |
| 139 | + safeErr.err = fmt.Errorf(format, args...) |
| 140 | + |
| 141 | + // Now create the redacted error by replacing all args with their redacted |
| 142 | + // version or by inserting a placeholder if the arg can't be redacted. |
| 143 | + for i := range a { |
| 144 | + switch t := a[i].(type) { |
| 145 | + case safe: |
| 146 | + args[i] = t.a |
| 147 | + case error: |
| 148 | + args[i] = Error(t) |
| 149 | + case redactor: |
| 150 | + args[i] = formatter(t.Redact()) |
| 151 | + default: |
| 152 | + args[i] = formatter(placeholder(t)) |
| 153 | + } |
| 154 | + } |
| 155 | + safeErr.redacted = fmt.Errorf(format, args...) |
| 156 | + return safeErr |
| 157 | +} |
| 158 | + |
| 159 | +// redactor defines the Redact interface for types that can format themselves |
| 160 | +// in redacted errors. |
| 161 | +type redactor interface { |
| 162 | + Redact() string |
| 163 | +} |
| 164 | + |
| 165 | +// safe wraps a value that is marked as safe for including in a redacted error. |
| 166 | +type safe struct{ a any } |
| 167 | + |
| 168 | +// Safe marks a value as safe for including in a redacted error. |
| 169 | +func Safe(a any) any { |
| 170 | + return safe{a} |
| 171 | +} |
| 172 | + |
| 173 | +// safeError is an error that can redact its message. |
| 174 | +type safeError struct { |
| 175 | + err error |
| 176 | + redacted error |
| 177 | + callers []uintptr |
| 178 | +} |
| 179 | + |
| 180 | +func (e *safeError) Error() string { return e.err.Error() } |
| 181 | +func (e *safeError) Redact() string { return e.redacted.Error() } |
| 182 | +func (e *safeError) Unwrap() error { return e.err } |
| 183 | + |
| 184 | +func (e *safeError) StackTrace() []runtime.Frame { |
| 185 | + if len(e.callers) == 0 { |
| 186 | + return nil |
| 187 | + } |
| 188 | + frameIter := runtime.CallersFrames(e.callers) |
| 189 | + frames := make([]runtime.Frame, 0, len(e.callers)) |
| 190 | + for { |
| 191 | + frame, more := frameIter.Next() |
| 192 | + frames = append(frames, frame) |
| 193 | + if !more { |
| 194 | + break |
| 195 | + } |
| 196 | + } |
| 197 | + return frames |
| 198 | +} |
| 199 | + |
| 200 | +func (e *safeError) Format(f fmt.State, verb rune) { |
| 201 | + switch verb { |
| 202 | + case 'v', 's': |
| 203 | + f.Write([]byte(e.Error())) |
| 204 | + if f.Flag('+') { |
| 205 | + for _, fr := range e.StackTrace() { |
| 206 | + fmt.Fprintf(f, "\n%s\n\t%s:%d", fr.Function, fr.File, fr.Line) |
| 207 | + } |
| 208 | + return |
| 209 | + } |
| 210 | + case 'q': |
| 211 | + fmt.Fprintf(f, "%q", e.Error()) |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +// redactedError is an error containing a redacted message. It is usually the |
| 216 | +// result of calling Error(safeError). |
| 217 | +type redactedError struct { |
| 218 | + msg string |
| 219 | + wrapped error |
| 220 | +} |
| 221 | + |
| 222 | +func (e *redactedError) Error() string { return e.msg } |
| 223 | +func (e *redactedError) Unwrap() error { return e.wrapped } |
| 224 | + |
| 225 | +// formatter allows a string to be formatted by any fmt verb. |
| 226 | +// For example, fmt.Sprintf("%d", formatter("100")) will return "100" without |
| 227 | +// an error. |
| 228 | +type formatter string |
| 229 | + |
| 230 | +func (f formatter) Format(s fmt.State, verb rune) { |
| 231 | + s.Write([]byte(f)) |
| 232 | +} |
| 233 | + |
| 234 | +// placeholder generates a placeholder string for values that don't satisfy |
| 235 | +// redactor. |
| 236 | +func placeholder(a any) string { |
| 237 | + return fmt.Sprintf("<redacted %T>", a) |
| 238 | +} |
0 commit comments