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:
parent
8b95c81488
commit
ef177c1c63
|
@ -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 == ""
|
||||
}
|
|
@ -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())
|
||||
}
|
Loading…
Reference in New Issue