Skip to content

Commit

Permalink
feat_: SensitiveString type (#6190)
Browse files Browse the repository at this point in the history
* feat_: SensitiveString type

* chore_: New by value, remove SetValue, add IsEmpty

* feat_: export RedactionPlaceholder

* fix_: MarshalJSON by value

* fix_: method receivers

* fix_: linter
  • Loading branch information
igor-sirotin authored Dec 11, 2024
1 parent 8b95c81 commit ef177c1
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 0 deletions.
58 changes: 58 additions & 0 deletions internal/security/sensitive_string.go
Original file line number Diff line number Diff line change
@@ -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 == ""
}
66 changes: 66 additions & 0 deletions internal/security/sensitive_string_test.go
Original file line number Diff line number Diff line change
@@ -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())
}

0 comments on commit ef177c1

Please sign in to comment.