Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat_: SensitiveString type #6190

Merged
merged 7 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
osmaczko marked this conversation as resolved.
Show resolved Hide resolved
}

// 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())
}
Loading