Skip to content
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
31 changes: 23 additions & 8 deletions encrypted_bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,12 @@ import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"

"gorm.io/gorm"
"gorm.io/gorm/schema"
)

var (
_ driver.Valuer = &EncryptedBytes{}
_ sql.Scanner = &EncryptedBytes{}
_ json.Marshaler = &EncryptedBytes{}
_ json.Unmarshaler = &EncryptedBytes{}
)

func NewEncryptedBytes(s string) EncryptedBytes {
e := &EncryptedBytes{}

Expand Down Expand Up @@ -50,7 +44,20 @@ func (e *EncryptedBytes) GormDBDataType(db *gorm.DB, field *schema.Field) string
}
}

// String intentionally returns a redacted placeholder to
// safeguard the plaintext from being inadventerly leaked
// in logs, stack traces, etc.
//
// To deliberately access the plaintext, call Plaintext() or Bytes().
func (e EncryptedBytes) String() string {
return "[REDACTED]"
}

// Plaintext returns the decrypted contents as a string.
//
// It is the caller's responsibility to ensure this value is
// not logged or otherwise exposed.
func (e EncryptedBytes) Plaintext() string {
return string(e)
}

Expand All @@ -59,7 +66,7 @@ func (e EncryptedBytes) Bytes() []byte {
}

// Scan implements the scanner interface
func (e *EncryptedBytes) Scan(value interface{}) error {
func (e *EncryptedBytes) Scan(value any) error {
b, ok := value.([]byte)
if !ok {
return errors.New("failed to read value as bytes")
Expand Down Expand Up @@ -122,3 +129,11 @@ func (e *EncryptedBytes) UnmarshalJSON(data []byte) error {

return e.Scan(b)
}

var (
_ driver.Valuer = &EncryptedBytes{}
_ sql.Scanner = &EncryptedBytes{}
_ json.Marshaler = &EncryptedBytes{}
_ json.Unmarshaler = &EncryptedBytes{}
_ fmt.Stringer = &EncryptedBytes{}
)
152 changes: 149 additions & 3 deletions encrypted_bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sqlcrypter

import (
"encoding/json"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -19,8 +20,153 @@ func Test_NewEncryptedBytes(t *testing.T) {
t.Run("success", func(t *testing.T) {
s := "Hello World"
e := NewEncryptedBytes(s)
assert.Equal(t, s, e.String())
assert.Equal(t, s, e.Plaintext())
})

t.Run("String redacts", func(t *testing.T) {
e := NewEncryptedBytes("super secret")
assert.Equal(t, "[REDACTED]", e.String())
})
}

func Test_EncryptedBytes_String(t *testing.T) {
secret := "do-not-leak-this-credential"
e := NewEncryptedBytes(secret)

out := fmt.Sprint(e)
assert.NotContains(t, out, secret, "formatted output must not contain plaintext")
assert.Equal(t, "[REDACTED]", out)
}

func Test_EncryptedBytes_Plaintext(t *testing.T) {
tests := []struct {
name string
value func() EncryptedBytes
want string
nilBuf bool
}{
{
name: "empty",
value: func() EncryptedBytes { return NewEncryptedBytes("") },
want: "",
nilBuf: true,
},
{
name: "non_empty",
value: func() EncryptedBytes { return NewEncryptedBytes("Hello World") },
want: "Hello World",
},
{
name: "unicode",
value: func() EncryptedBytes { return NewEncryptedBytes("你好世界") },
want: "你好世界",
},
{
name: "nil_slice",
value: func() EncryptedBytes {
var e EncryptedBytes
return e
},
want: "",
nilBuf: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := tt.value()
assert.Equal(t, tt.want, e.Plaintext())
if tt.nilBuf {
assert.Nil(t, []byte(e))
}
})
}
}

func Test_EncryptedBytes_Bytes(t *testing.T) {
tests := []struct {
name string
value func() EncryptedBytes
want []byte
nilBuf bool
mutateFirst bool // mutating returned slice updates backing EncryptedBytes
afterMutate string
}{
{
name: "empty",
value: func() EncryptedBytes { return NewEncryptedBytes("") },
want: nil,
nilBuf: true,
},
{
name: "non_empty",
value: func() EncryptedBytes { return NewEncryptedBytes("Hello World") },
want: []byte("Hello World"),
mutateFirst: true,
afterMutate: "Jello World",
},
{
name: "unicode",
value: func() EncryptedBytes { return NewEncryptedBytes("你好世界") },
want: []byte("你好世界"),
},
{
name: "nil_slice",
value: func() EncryptedBytes {
var e EncryptedBytes
return e
},
want: nil,
nilBuf: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := tt.value()
got := e.Bytes()
assert.Equal(t, tt.want, got)
if tt.nilBuf {
assert.Nil(t, got)
}
if tt.mutateFirst {
require.NotEmpty(t, got)
got[0] = 'J'
assert.Equal(t, tt.afterMutate, e.Plaintext())
}
})
}
}

func Test_EncryptedBytes_GormDataType(t *testing.T) {
tests := []struct {
name string
value func() EncryptedBytes
}{
{
name: "with_plaintext",
value: func() EncryptedBytes { return NewEncryptedBytes("secret") },
},
{
name: "empty_constructor",
value: func() EncryptedBytes { return NewEncryptedBytes("") },
},
{
name: "nil_slice",
value: func() EncryptedBytes {
var e EncryptedBytes
return e
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := tt.value()
p := &e
assert.Equal(t, "encryptedbytes", p.GormDataType())
})
}
}

func Test_EncryptedBytes_Scan(t *testing.T) {
Expand All @@ -45,7 +191,7 @@ func Test_EncryptedBytes_Scan(t *testing.T) {
e := &EncryptedBytes{}
err := e.Scan([]byte("SGVsbG8gV29ybGQ="))
require.NoError(t, err)
assert.Equal(t, "Hello World", e.String())
assert.Equal(t, "Hello World", e.Plaintext())
})
}

Expand Down Expand Up @@ -96,5 +242,5 @@ func Test_EncryptedBytes_UnmarshalJSON(t *testing.T) {

err := json.Unmarshal(data, &e)
require.NoError(t, err)
assert.Equal(t, "Hello World", e.Secret.String())
assert.Equal(t, "Hello World", e.Secret.Plaintext())
}
43 changes: 43 additions & 0 deletions random_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package sqlcrypter

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGenerateBytes(t *testing.T) {
tests := []struct {
name string
n int
wantLen int
}{
{name: "n_zero", n: 0, wantLen: 0},
{name: "n_one", n: 1, wantLen: 1},
{name: "n_16", n: 16, wantLen: 16},
{name: "n_256", n: 256, wantLen: 256},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GenerateBytes(tt.n)
require.NoError(t, err)
require.Len(t, got, tt.wantLen)
})
}

t.Run("distinct_outputs", func(t *testing.T) {
a, err := GenerateBytes(32)
require.NoError(t, err)
b, err := GenerateBytes(32)
require.NoError(t, err)
assert.NotEqual(t, a, b, "two 32-byte draws from crypto/rand should almost never match")
})
}

func TestGenerateBytes_Panics(t *testing.T) {
assert.Panics(t, func() {
_, _ = GenerateBytes(-1)
})
}
Loading