From ef177c1c631ea56c2a5506e66958b01eab5fd09f Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Wed, 11 Dec 2024 14:07:34 +0000 Subject: [PATCH] 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 --- internal/security/sensitive_string.go | 58 +++++++++++++++++++ internal/security/sensitive_string_test.go | 66 ++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 internal/security/sensitive_string.go create mode 100644 internal/security/sensitive_string_test.go diff --git a/internal/security/sensitive_string.go b/internal/security/sensitive_string.go new file mode 100644 index 000000000..70387ed4e --- /dev/null +++ b/internal/security/sensitive_string.go @@ -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 == "" +} diff --git a/internal/security/sensitive_string_test.go b/internal/security/sensitive_string_test.go new file mode 100644 index 000000000..dd843260c --- /dev/null +++ b/internal/security/sensitive_string_test.go @@ -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()) +}