Skip to content

Commit ef177c1

Browse files
authored
feat_: SensitiveString type (#6190)
* feat_: SensitiveString type * chore_: New by value, remove SetValue, add IsEmpty * feat_: export RedactionPlaceholder * fix_: MarshalJSON by value * fix_: method receivers * fix_: linter
1 parent 8b95c81 commit ef177c1

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

internal/security/sensitive_string.go

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package security
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
const RedactionPlaceholder = "***"
8+
9+
// SensitiveString is a type for handling sensitive information securely.
10+
// This helps to achieve the following goals:
11+
// 1. Prevent accidental logging of sensitive information.
12+
// 2. Provide controlled visibility (e.g., redacted output for String() or MarshalJSON()).
13+
// 3. Enable controlled access to the sensitive value when needed.
14+
type SensitiveString struct {
15+
value string
16+
}
17+
18+
// NewSensitiveString creates a new SensitiveString
19+
func NewSensitiveString(value string) SensitiveString {
20+
return SensitiveString{value: value}
21+
}
22+
23+
// String provides a redacted version of the sensitive string
24+
func (s SensitiveString) String() string {
25+
if s.value == "" {
26+
return ""
27+
}
28+
return RedactionPlaceholder
29+
}
30+
31+
// MarshalJSON ensures that sensitive strings are redacted when marshaled to JSON
32+
// NOTE: It's important to define this method on the value receiver,
33+
// otherwise `json.Marshal` will not call this method.
34+
func (s SensitiveString) MarshalJSON() ([]byte, error) {
35+
return json.Marshal(s.String())
36+
}
37+
38+
// UnmarshalJSON implements unmarshalling a sensitive string from JSON
39+
// NOTE: It's important to define this method on the pointer receiver,
40+
// otherwise `json.Marshal` will not call this method.
41+
func (s *SensitiveString) UnmarshalJSON(data []byte) error {
42+
var value string
43+
if err := json.Unmarshal(data, &value); err != nil {
44+
return err
45+
}
46+
s.value = value
47+
return nil
48+
}
49+
50+
// Reveal exposes the sensitive value (use with caution)
51+
func (s SensitiveString) Reveal() string {
52+
return s.value
53+
}
54+
55+
// Empty checks if the value is empty
56+
func (s SensitiveString) Empty() bool {
57+
return s.value == ""
58+
}
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package security
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/brianvoe/gofakeit/v6"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestNewSensitiveString(t *testing.T) {
12+
secretValue := gofakeit.LetterN(10)
13+
s := NewSensitiveString(secretValue)
14+
require.Equal(t, secretValue, s.Reveal())
15+
}
16+
17+
func TestStringRedaction(t *testing.T) {
18+
secretValue := gofakeit.LetterN(10)
19+
s := NewSensitiveString(secretValue)
20+
require.Equal(t, RedactionPlaceholder, s.String())
21+
}
22+
23+
func TestEmptyStringRedaction(t *testing.T) {
24+
s := NewSensitiveString("")
25+
require.Equal(t, "", s.String())
26+
}
27+
28+
func TestMarshalJSON(t *testing.T) {
29+
secretValue := gofakeit.LetterN(10)
30+
s := NewSensitiveString(secretValue)
31+
data, err := json.Marshal(s)
32+
require.NoError(t, err)
33+
require.JSONEq(t, `"`+RedactionPlaceholder+`"`, string(data))
34+
}
35+
36+
func TestMarshalJSONPointer(t *testing.T) {
37+
secretValue := gofakeit.LetterN(10)
38+
s := NewSensitiveString(secretValue)
39+
data, err := json.Marshal(&s)
40+
require.NoError(t, err)
41+
require.JSONEq(t, `"`+RedactionPlaceholder+`"`, string(data))
42+
}
43+
44+
func TestUnmarshalJSON(t *testing.T) {
45+
secretValue := gofakeit.LetterN(10)
46+
data := `"` + secretValue + `"`
47+
var s SensitiveString
48+
err := json.Unmarshal([]byte(data), &s)
49+
require.NoError(t, err)
50+
require.Equal(t, secretValue, s.Reveal())
51+
}
52+
53+
func TestUnamarshalJSONError(t *testing.T) {
54+
// Can't unmarshal a non-string value
55+
var s SensitiveString
56+
data := `{"key": "value"}`
57+
err := json.Unmarshal([]byte(data), &s)
58+
require.Error(t, err)
59+
}
60+
61+
func TestCopySensitiveString(t *testing.T) {
62+
secretValue := gofakeit.LetterN(10)
63+
s := NewSensitiveString(secretValue)
64+
sCopy := s
65+
require.Equal(t, secretValue, sCopy.Reveal())
66+
}

0 commit comments

Comments
 (0)