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
This commit is contained in:
Igor Sirotin 2024-12-11 14:07:34 +00:00 committed by GitHub
parent 8b95c81488
commit ef177c1c63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 124 additions and 0 deletions

View File

@ -0,0 +1,58 @@
package security
import (
"encoding/json"
)
const RedactionPlaceholder = "***"
// SensitiveString is a type for handling sensitive information securely.
// This helps to achieve the following goals:
// 1. Prevent accidental logging of sensitive information.
// 2. Provide controlled visibility (e.g., redacted output for String() or MarshalJSON()).
// 3. Enable controlled access to the sensitive value when needed.
type SensitiveString struct {
value string
}
// NewSensitiveString creates a new SensitiveString
func NewSensitiveString(value string) SensitiveString {
return SensitiveString{value: value}
}
// String provides a redacted version of the sensitive string
func (s SensitiveString) String() string {
if s.value == "" {
return ""
}
return RedactionPlaceholder
}
// MarshalJSON ensures that sensitive strings are redacted when marshaled to JSON
// NOTE: It's important to define this method on the value receiver,
// otherwise `json.Marshal` will not call this method.
func (s SensitiveString) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// UnmarshalJSON implements unmarshalling a sensitive string from JSON
// NOTE: It's important to define this method on the pointer receiver,
// otherwise `json.Marshal` will not call this method.
func (s *SensitiveString) UnmarshalJSON(data []byte) error {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
s.value = value
return nil
}
// Reveal exposes the sensitive value (use with caution)
func (s SensitiveString) Reveal() string {
return s.value
}
// Empty checks if the value is empty
func (s SensitiveString) Empty() bool {
return s.value == ""
}

View File

@ -0,0 +1,66 @@
package security
import (
"encoding/json"
"testing"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/require"
)
func TestNewSensitiveString(t *testing.T) {
secretValue := gofakeit.LetterN(10)
s := NewSensitiveString(secretValue)
require.Equal(t, secretValue, s.Reveal())
}
func TestStringRedaction(t *testing.T) {
secretValue := gofakeit.LetterN(10)
s := NewSensitiveString(secretValue)
require.Equal(t, RedactionPlaceholder, s.String())
}
func TestEmptyStringRedaction(t *testing.T) {
s := NewSensitiveString("")
require.Equal(t, "", s.String())
}
func TestMarshalJSON(t *testing.T) {
secretValue := gofakeit.LetterN(10)
s := NewSensitiveString(secretValue)
data, err := json.Marshal(s)
require.NoError(t, err)
require.JSONEq(t, `"`+RedactionPlaceholder+`"`, string(data))
}
func TestMarshalJSONPointer(t *testing.T) {
secretValue := gofakeit.LetterN(10)
s := NewSensitiveString(secretValue)
data, err := json.Marshal(&s)
require.NoError(t, err)
require.JSONEq(t, `"`+RedactionPlaceholder+`"`, string(data))
}
func TestUnmarshalJSON(t *testing.T) {
secretValue := gofakeit.LetterN(10)
data := `"` + secretValue + `"`
var s SensitiveString
err := json.Unmarshal([]byte(data), &s)
require.NoError(t, err)
require.Equal(t, secretValue, s.Reveal())
}
func TestUnamarshalJSONError(t *testing.T) {
// Can't unmarshal a non-string value
var s SensitiveString
data := `{"key": "value"}`
err := json.Unmarshal([]byte(data), &s)
require.Error(t, err)
}
func TestCopySensitiveString(t *testing.T) {
secretValue := gofakeit.LetterN(10)
s := NewSensitiveString(secretValue)
sCopy := s
require.Equal(t, secretValue, sCopy.Reveal())
}