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