No commit suggestions generated

This commit is contained in:
2025-10-09 15:10:39 -04:00
commit a934caa7d3
323 changed files with 98121 additions and 0 deletions

325
secure/memory.go Normal file
View File

@@ -0,0 +1,325 @@
// Package secure provides utilities for secure memory handling and sensitive data management.
// It includes functions for explicit memory zeroization and secure data lifecycle management.
package secure
import (
"crypto/rand"
"fmt"
"runtime"
"sync"
)
// SecureBytes wraps a byte slice with automatic cleanup functionality
type SecureBytes struct {
data []byte
mu sync.RWMutex
finalized bool
}
// NewSecureBytes creates a new SecureBytes instance with automatic cleanup
func NewSecureBytes(size int) *SecureBytes {
if size <= 0 {
return &SecureBytes{data: nil}
}
sb := &SecureBytes{
data: make([]byte, size),
}
// Set finalizer for automatic cleanup if Clear() is not called
runtime.SetFinalizer(sb, (*SecureBytes).finalize)
return sb
}
// FromBytes creates a SecureBytes instance from existing data (copies the data)
func FromBytes(data []byte) *SecureBytes {
if len(data) == 0 {
return &SecureBytes{data: nil}
}
sb := &SecureBytes{
data: make([]byte, len(data)),
}
copy(sb.data, data)
runtime.SetFinalizer(sb, (*SecureBytes).finalize)
return sb
}
// Bytes returns a copy of the secure data to prevent external modification
func (sb *SecureBytes) Bytes() []byte {
sb.mu.RLock()
defer sb.mu.RUnlock()
if sb.data == nil {
return nil
}
result := make([]byte, len(sb.data))
copy(result, sb.data)
return result
}
// Size returns the size of the secure data
func (sb *SecureBytes) Size() int {
sb.mu.RLock()
defer sb.mu.RUnlock()
if sb.data == nil {
return 0
}
return len(sb.data)
}
// IsEmpty checks if the secure data is empty or nil
func (sb *SecureBytes) IsEmpty() bool {
sb.mu.RLock()
defer sb.mu.RUnlock()
return sb.data == nil || len(sb.data) == 0
}
// Clear explicitly zeros the memory and removes the finalizer
func (sb *SecureBytes) Clear() {
sb.mu.Lock()
defer sb.mu.Unlock()
if !sb.finalized && sb.data != nil {
Zeroize(sb.data)
sb.data = nil
sb.finalized = true
runtime.SetFinalizer(sb, nil) // Remove finalizer since we've cleaned up
}
}
// finalize is called by the garbage collector if Clear() was not called
func (sb *SecureBytes) finalize() {
sb.Clear()
}
// CopyTo safely copies data to the secure buffer
func (sb *SecureBytes) CopyTo(data []byte) error {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.finalized {
return fmt.Errorf("secure bytes has been finalized")
}
if sb.data == nil {
return fmt.Errorf("secure bytes is nil")
}
if len(data) > len(sb.data) {
return fmt.Errorf("data size %d exceeds secure buffer size %d", len(data), len(sb.data))
}
// Zero existing data first
Zeroize(sb.data)
copy(sb.data, data)
return nil
}
// Zeroize explicitly zeros out sensitive data from memory using byte slicing
func Zeroize(data []byte) {
if len(data) == 0 {
return
}
// Explicitly zero each byte to prevent compiler optimizations
for i := range data {
data[i] = 0
}
// Force memory barrier to ensure zeroization is not optimized away
runtime.KeepAlive(data)
}
// ZeroizeString attempts to clear a string reference (limited effectiveness)
// Note: Go strings are immutable, so this only clears the reference, not the underlying data
func ZeroizeString(s *string) {
if s == nil {
return
}
// Simply clear the reference - Go strings are immutable
*s = ""
}
// SecureString wraps a string with secure cleanup capabilities
type SecureString struct {
value string
mu sync.RWMutex
finalized bool
}
// NewSecureString creates a new SecureString with automatic cleanup
func NewSecureString(s string) *SecureString {
ss := &SecureString{
value: s,
}
runtime.SetFinalizer(ss, (*SecureString).finalize)
return ss
}
// String returns the secure string value
func (ss *SecureString) String() string {
ss.mu.RLock()
defer ss.mu.RUnlock()
if ss.finalized {
return ""
}
return ss.value
}
// Clear attempts to zero the string and marks it as finalized
func (ss *SecureString) Clear() {
ss.mu.Lock()
defer ss.mu.Unlock()
if !ss.finalized {
ZeroizeString(&ss.value)
ss.value = ""
ss.finalized = true
runtime.SetFinalizer(ss, nil)
}
}
// finalize is called by the garbage collector
func (ss *SecureString) finalize() {
ss.Clear()
}
// IsEmpty checks if the secure string is empty
func (ss *SecureString) IsEmpty() bool {
ss.mu.RLock()
defer ss.mu.RUnlock()
return ss.finalized || ss.value == ""
}
// SecureBuffer provides a reusable buffer for sensitive operations
type SecureBuffer struct {
buffer []byte
mu sync.Mutex
}
// NewSecureBuffer creates a new secure buffer with the specified capacity
func NewSecureBuffer(capacity int) *SecureBuffer {
if capacity <= 0 {
capacity = 1024 // Default capacity
}
sb := &SecureBuffer{
buffer: make([]byte, 0, capacity),
}
runtime.SetFinalizer(sb, (*SecureBuffer).finalize)
return sb
}
// Write appends data to the secure buffer
func (sb *SecureBuffer) Write(data []byte) error {
sb.mu.Lock()
defer sb.mu.Unlock()
if len(sb.buffer)+len(data) > cap(sb.buffer) {
return fmt.Errorf("buffer overflow: capacity %d, current size %d, write size %d",
cap(sb.buffer), len(sb.buffer), len(data))
}
sb.buffer = append(sb.buffer, data...)
return nil
}
// Read returns a copy of the buffer contents
func (sb *SecureBuffer) Read() []byte {
sb.mu.Lock()
defer sb.mu.Unlock()
result := make([]byte, len(sb.buffer))
copy(result, sb.buffer)
return result
}
// Reset clears the buffer contents but maintains capacity
func (sb *SecureBuffer) Reset() {
sb.mu.Lock()
defer sb.mu.Unlock()
if len(sb.buffer) > 0 {
Zeroize(sb.buffer[:cap(sb.buffer)]) // Zero the entire backing array
sb.buffer = sb.buffer[:0] // Reset length to 0
}
}
// Clear zeros the buffer and releases memory
func (sb *SecureBuffer) Clear() {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.buffer != nil {
Zeroize(sb.buffer[:cap(sb.buffer)]) // Zero entire backing array
sb.buffer = nil
runtime.SetFinalizer(sb, nil)
}
}
// finalize is called by the garbage collector
func (sb *SecureBuffer) finalize() {
sb.Clear()
}
// Size returns the current size of data in the buffer
func (sb *SecureBuffer) Size() int {
sb.mu.Lock()
defer sb.mu.Unlock()
return len(sb.buffer)
}
// Capacity returns the maximum capacity of the buffer
func (sb *SecureBuffer) Capacity() int {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.buffer == nil {
return 0
}
return cap(sb.buffer)
}
// ZeroizeMultiple zeros multiple byte slices in a single call
func ZeroizeMultiple(slices ...[]byte) {
for _, slice := range slices {
Zeroize(slice)
}
}
// SecureCompare performs constant-time comparison of two byte slices
// Returns true if the slices are equal, false otherwise
func SecureCompare(a, b []byte) bool {
if len(a) != len(b) {
return false
}
var result byte
for i := 0; i < len(a); i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
// SecureRandom fills the provided slice with cryptographically secure random bytes
func SecureRandom(data []byte) error {
if len(data) == 0 {
return nil
}
// Use Go's crypto/rand for secure random generation
if _, err := rand.Read(data); err != nil {
return fmt.Errorf("failed to generate secure random bytes: %w", err)
}
return nil
}

523
secure/memory_test.go Normal file
View File

@@ -0,0 +1,523 @@
package secure
import (
"bytes"
"fmt"
"runtime"
"testing"
"time"
)
func TestZeroize(t *testing.T) {
tests := []struct {
name string
data []byte
}{
{"empty slice", []byte{}},
{"single byte", []byte{0xFF}},
{"small slice", []byte{1, 2, 3, 4, 5}},
{"large slice", make([]byte, 1024)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Fill with non-zero data
for i := range tt.data {
tt.data[i] = byte(i%256 + 1)
}
// Store original for verification
original := make([]byte, len(tt.data))
copy(original, tt.data)
// Zeroize
Zeroize(tt.data)
// Verify all bytes are zero
for i, b := range tt.data {
if b != 0 {
t.Errorf("Byte at index %d not zeroed: got %d, want 0", i, b)
}
}
// Verify original data was actually non-zero (for non-empty slices)
if len(original) > 0 {
hasNonZero := false
for _, b := range original {
if b != 0 {
hasNonZero = true
break
}
}
if !hasNonZero {
t.Error("Test data was already all zeros - invalid test")
}
}
})
}
}
func TestSecureBytes(t *testing.T) {
t.Run("NewSecureBytes", func(t *testing.T) {
sb := NewSecureBytes(32)
if sb == nil {
t.Fatal("NewSecureBytes returned nil")
}
if sb.Size() != 32 {
t.Errorf("Size() = %d, want 32", sb.Size())
}
if sb.IsEmpty() {
t.Error("NewSecureBytes should not be empty")
}
sb.Clear()
})
t.Run("zero size", func(t *testing.T) {
sb := NewSecureBytes(0)
if sb.Size() != 0 {
t.Errorf("Size() = %d, want 0", sb.Size())
}
if !sb.IsEmpty() {
t.Error("Zero-size SecureBytes should be empty")
}
})
t.Run("FromBytes", func(t *testing.T) {
original := []byte{1, 2, 3, 4, 5}
sb := FromBytes(original)
if sb.Size() != len(original) {
t.Errorf("Size() = %d, want %d", sb.Size(), len(original))
}
retrieved := sb.Bytes()
if !bytes.Equal(retrieved, original) {
t.Error("Retrieved bytes don't match original")
}
// Verify independence - modifying original shouldn't affect SecureBytes
original[0] = 99
retrieved2 := sb.Bytes()
if retrieved2[0] == 99 {
t.Error("SecureBytes was affected by external modification")
}
sb.Clear()
})
t.Run("Bytes returns copy", func(t *testing.T) {
sb := NewSecureBytes(16)
bytes1 := sb.Bytes()
bytes2 := sb.Bytes()
// Should be equal content
if !bytes.Equal(bytes1, bytes2) {
t.Error("Multiple Bytes() calls returned different content")
}
// Should be different slices
if len(bytes1) > 0 && &bytes1[0] == &bytes2[0] {
t.Error("Bytes() returned same underlying array")
}
// Modifying returned slice shouldn't affect SecureBytes
if len(bytes1) > 0 {
bytes1[0] = 0xFF
bytes3 := sb.Bytes()
if bytes3[0] == 0xFF {
t.Error("External modification affected SecureBytes")
}
}
sb.Clear()
})
t.Run("Clear", func(t *testing.T) {
data := []byte{1, 2, 3, 4, 5}
sb := FromBytes(data)
if sb.IsEmpty() {
t.Error("SecureBytes should not be empty before Clear")
}
sb.Clear()
if !sb.IsEmpty() {
t.Error("SecureBytes should be empty after Clear")
}
if sb.Size() != 0 {
t.Error("Size should be 0 after Clear")
}
bytes := sb.Bytes()
if bytes != nil {
t.Error("Bytes() should return nil after Clear")
}
})
t.Run("CopyTo", func(t *testing.T) {
sb := NewSecureBytes(10)
data := []byte{1, 2, 3, 4, 5}
err := sb.CopyTo(data)
if err != nil {
t.Fatalf("CopyTo failed: %v", err)
}
retrieved := sb.Bytes()
if !bytes.Equal(retrieved[:len(data)], data) {
t.Error("CopyTo didn't copy data correctly")
}
// Test overflow
largeData := make([]byte, 20)
err = sb.CopyTo(largeData)
if err == nil {
t.Error("CopyTo should fail with oversized data")
}
sb.Clear()
// Test copy to finalized
err = sb.CopyTo(data)
if err == nil {
t.Error("CopyTo should fail on finalized SecureBytes")
}
})
}
func TestSecureString(t *testing.T) {
t.Run("basic operations", func(t *testing.T) {
original := "sensitive data"
ss := NewSecureString(original)
if ss.String() != original {
t.Error("String() doesn't match original")
}
if ss.IsEmpty() {
t.Error("SecureString should not be empty")
}
ss.Clear()
if !ss.IsEmpty() {
t.Error("SecureString should be empty after Clear")
}
if ss.String() != "" {
t.Error("String() should return empty string after Clear")
}
})
t.Run("empty string", func(t *testing.T) {
ss := NewSecureString("")
if !ss.IsEmpty() {
t.Error("Empty SecureString should report as empty")
}
ss.Clear()
})
}
func TestSecureBuffer(t *testing.T) {
t.Run("basic operations", func(t *testing.T) {
sb := NewSecureBuffer(100)
if sb.Size() != 0 {
t.Error("New buffer should have size 0")
}
if sb.Capacity() != 100 {
t.Errorf("Capacity() = %d, want 100", sb.Capacity())
}
// Write data
data1 := []byte("hello")
err := sb.Write(data1)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if sb.Size() != len(data1) {
t.Errorf("Size() = %d, want %d", sb.Size(), len(data1))
}
// Write more data
data2 := []byte(" world")
err = sb.Write(data2)
if err != nil {
t.Fatalf("Second write failed: %v", err)
}
expected := append(data1, data2...)
result := sb.Read()
if !bytes.Equal(result, expected) {
t.Errorf("Read() = %q, want %q", string(result), string(expected))
}
sb.Clear()
})
t.Run("overflow protection", func(t *testing.T) {
sb := NewSecureBuffer(10)
// Fill to capacity
data := make([]byte, 10)
err := sb.Write(data)
if err != nil {
t.Fatalf("Write to capacity failed: %v", err)
}
// Try to overflow
err = sb.Write([]byte{1})
if err == nil {
t.Error("Write should fail on buffer overflow")
}
sb.Clear()
})
t.Run("reset", func(t *testing.T) {
sb := NewSecureBuffer(50)
data := []byte("test data")
err := sb.Write(data)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
if sb.Size() == 0 {
t.Error("Buffer should not be empty before reset")
}
sb.Reset()
if sb.Size() != 0 {
t.Error("Buffer should be empty after reset")
}
if sb.Capacity() != 50 {
t.Error("Capacity should be preserved after reset")
}
// Should be able to write again
err = sb.Write([]byte("new data"))
if err != nil {
t.Error("Should be able to write after reset")
}
sb.Clear()
})
}
func TestZeroizeMultiple(t *testing.T) {
slice1 := []byte{1, 2, 3}
slice2 := []byte{4, 5, 6}
slice3 := []byte{7, 8, 9}
ZeroizeMultiple(slice1, slice2, slice3)
slices := [][]byte{slice1, slice2, slice3}
for i, slice := range slices {
for j, b := range slice {
if b != 0 {
t.Errorf("Slice %d, byte %d not zeroed: got %d", i, j, b)
}
}
}
}
func TestSecureCompare(t *testing.T) {
tests := []struct {
name string
a, b []byte
expected bool
}{
{"equal slices", []byte{1, 2, 3}, []byte{1, 2, 3}, true},
{"different content", []byte{1, 2, 3}, []byte{1, 2, 4}, false},
{"different length", []byte{1, 2, 3}, []byte{1, 2}, false},
{"both empty", []byte{}, []byte{}, true},
{"one empty", []byte{1}, []byte{}, false},
{"both nil", nil, nil, true},
{"one nil", []byte{1}, nil, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SecureCompare(tt.a, tt.b)
if result != tt.expected {
t.Errorf("SecureCompare() = %v, want %v", result, tt.expected)
}
})
}
}
func TestSecureRandom(t *testing.T) {
sizes := []int{0, 1, 16, 32, 1024}
for _, size := range sizes {
t.Run(fmt.Sprintf("%d bytes", size), func(t *testing.T) {
data := make([]byte, size)
err := SecureRandom(data)
if err != nil {
t.Fatalf("SecureRandom failed: %v", err)
}
if size == 0 {
return // Nothing to verify for empty slice
}
// For non-zero sizes, verify we got some randomness
// (Note: there's a tiny chance this could fail with truly random data)
allZeros := true
allSame := true
first := data[0]
for _, b := range data {
if b != 0 {
allZeros = false
}
if b != first {
allSame = false
}
}
if size > 1 {
if allZeros {
t.Error("SecureRandom returned all zeros (suspicious)")
}
if allSame {
t.Error("SecureRandom returned all same values (suspicious)")
}
}
})
}
}
// Test that finalizers work correctly (this is tricky to test reliably)
func TestFinalizers(t *testing.T) {
t.Run("SecureBytes finalizer", func(t *testing.T) {
// Create a SecureBytes and let it go out of scope
func() {
sb := NewSecureBytes(32)
// Fill with test data
data := make([]byte, 32)
for i := range data {
data[i] = byte(i + 1)
}
sb.CopyTo(data)
// sb goes out of scope here
}()
// Force garbage collection
runtime.GC()
runtime.GC()
time.Sleep(10 * time.Millisecond)
// We can't easily verify the finalizer ran, but this tests that
// the finalizer doesn't cause a panic
})
t.Run("SecureString finalizer", func(t *testing.T) {
func() {
ss := NewSecureString("test data")
_ = ss.String()
// ss goes out of scope here
}()
runtime.GC()
runtime.GC()
time.Sleep(10 * time.Millisecond)
})
t.Run("SecureBuffer finalizer", func(t *testing.T) {
func() {
sb := NewSecureBuffer(64)
sb.Write([]byte("test data"))
// sb goes out of scope here
}()
runtime.GC()
runtime.GC()
time.Sleep(10 * time.Millisecond)
})
}
func BenchmarkZeroize(b *testing.B) {
sizes := []int{32, 256, 1024, 4096}
for _, size := range sizes {
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
data := make([]byte, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Fill with data
for j := range data {
data[j] = byte(j)
}
// Zeroize
Zeroize(data)
}
})
}
}
func BenchmarkSecureCompare(b *testing.B) {
sizes := []int{16, 32, 256, 1024}
for _, size := range sizes {
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
a := make([]byte, size)
b_slice := make([]byte, size)
// Fill with identical data
for i := range a {
a[i] = byte(i)
b_slice[i] = byte(i)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
SecureCompare(a, b_slice)
}
})
}
}
func BenchmarkSecureBytes(b *testing.B) {
b.Run("NewSecureBytes", func(b *testing.B) {
for i := 0; i < b.N; i++ {
sb := NewSecureBytes(32)
sb.Clear()
}
})
b.Run("Bytes", func(b *testing.B) {
sb := NewSecureBytes(32)
defer sb.Clear()
b.ResetTimer()
for i := 0; i < b.N; i++ {
data := sb.Bytes()
_ = data
}
})
b.Run("CopyTo", func(b *testing.B) {
sb := NewSecureBytes(32)
defer sb.Clear()
testData := make([]byte, 16)
for i := range testData {
testData[i] = byte(i)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sb.CopyTo(testData)
}
})
}