mirror of
https://github.com/sonr-io/crypto.git
synced 2026-08-02 15:31:38 +00:00
No commit suggestions generated
This commit is contained in:
+227
@@ -0,0 +1,227 @@
|
||||
// Package salt provides cryptographically secure salt generation and management
|
||||
// for use in key derivation functions and encryption operations.
|
||||
package salt
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSaltSize defines the recommended salt size (256 bits)
|
||||
DefaultSaltSize = 32
|
||||
// MinSaltSize defines the minimum acceptable salt size (128 bits)
|
||||
MinSaltSize = 16
|
||||
// MaxSaltSize defines the maximum salt size to prevent resource exhaustion
|
||||
MaxSaltSize = 1024
|
||||
)
|
||||
|
||||
// Salt represents a cryptographically secure salt value
|
||||
type Salt struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
// Generate creates a new cryptographically secure salt of the specified size
|
||||
func Generate(size int) (*Salt, error) {
|
||||
if size < MinSaltSize {
|
||||
return nil, fmt.Errorf("salt size too small: minimum %d bytes required", MinSaltSize)
|
||||
}
|
||||
if size > MaxSaltSize {
|
||||
return nil, fmt.Errorf("salt size too large: maximum %d bytes allowed", MaxSaltSize)
|
||||
}
|
||||
|
||||
saltBytes := make([]byte, size)
|
||||
if _, err := io.ReadFull(rand.Reader, saltBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate random salt: %w", err)
|
||||
}
|
||||
|
||||
return &Salt{value: saltBytes}, nil
|
||||
}
|
||||
|
||||
// GenerateDefault creates a new salt with the default recommended size
|
||||
func GenerateDefault() (*Salt, error) {
|
||||
return Generate(DefaultSaltSize)
|
||||
}
|
||||
|
||||
// FromBytes creates a Salt from existing bytes (validates minimum size)
|
||||
func FromBytes(data []byte) (*Salt, error) {
|
||||
if len(data) < MinSaltSize {
|
||||
return nil, fmt.Errorf("salt too small: minimum %d bytes required, got %d", MinSaltSize, len(data))
|
||||
}
|
||||
if len(data) > MaxSaltSize {
|
||||
return nil, fmt.Errorf("salt too large: maximum %d bytes allowed, got %d", MaxSaltSize, len(data))
|
||||
}
|
||||
|
||||
// Copy to prevent external modification
|
||||
saltBytes := make([]byte, len(data))
|
||||
copy(saltBytes, data)
|
||||
|
||||
return &Salt{value: saltBytes}, nil
|
||||
}
|
||||
|
||||
// Bytes returns a copy of the salt bytes to prevent external modification
|
||||
func (s *Salt) Bytes() []byte {
|
||||
if s == nil || s.value == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]byte, len(s.value))
|
||||
copy(result, s.value)
|
||||
return result
|
||||
}
|
||||
|
||||
// Size returns the size of the salt in bytes
|
||||
func (s *Salt) Size() int {
|
||||
if s == nil || s.value == nil {
|
||||
return 0
|
||||
}
|
||||
return len(s.value)
|
||||
}
|
||||
|
||||
// String returns a redacted string representation for logging (does not expose salt value)
|
||||
func (s *Salt) String() string {
|
||||
if s == nil || s.value == nil {
|
||||
return "Salt{<nil>}"
|
||||
}
|
||||
return fmt.Sprintf("Salt{size=%d}", len(s.value))
|
||||
}
|
||||
|
||||
// Equal compares two salts in constant time to prevent timing attacks
|
||||
func (s *Salt) Equal(other *Salt) bool {
|
||||
if s == nil || other == nil {
|
||||
return s == other
|
||||
}
|
||||
if len(s.value) != len(other.value) {
|
||||
return false
|
||||
}
|
||||
return constantTimeCompare(s.value, other.value)
|
||||
}
|
||||
|
||||
// Clear securely zeros the salt value from memory
|
||||
func (s *Salt) Clear() {
|
||||
if s != nil && s.value != nil {
|
||||
for i := range s.value {
|
||||
s.value[i] = 0
|
||||
}
|
||||
s.value = nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmpty checks if the salt is nil or has zero length
|
||||
func (s *Salt) IsEmpty() bool {
|
||||
return s == nil || s.value == nil || len(s.value) == 0
|
||||
}
|
||||
|
||||
// constantTimeCompare performs constant-time comparison of two byte slices
|
||||
func constantTimeCompare(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
|
||||
}
|
||||
|
||||
// SaltStore manages storage and retrieval of salts with metadata
|
||||
type SaltStore struct {
|
||||
salts map[string]*Salt
|
||||
}
|
||||
|
||||
// NewSaltStore creates a new salt store for managing multiple salts
|
||||
func NewSaltStore() *SaltStore {
|
||||
return &SaltStore{
|
||||
salts: make(map[string]*Salt),
|
||||
}
|
||||
}
|
||||
|
||||
// Store saves a salt with the given identifier
|
||||
func (ss *SaltStore) Store(id string, salt *Salt) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("salt identifier cannot be empty")
|
||||
}
|
||||
if salt == nil || salt.IsEmpty() {
|
||||
return fmt.Errorf("salt cannot be nil or empty")
|
||||
}
|
||||
|
||||
// Store a copy to prevent external modification
|
||||
ss.salts[id] = &Salt{
|
||||
value: salt.Bytes(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieve gets a salt by its identifier
|
||||
func (ss *SaltStore) Retrieve(id string) (*Salt, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("salt identifier cannot be empty")
|
||||
}
|
||||
|
||||
salt, exists := ss.salts[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("salt not found for identifier: %s", id)
|
||||
}
|
||||
|
||||
// Return a copy to prevent external modification
|
||||
return &Salt{
|
||||
value: salt.Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Remove deletes a salt from the store and clears its memory
|
||||
func (ss *SaltStore) Remove(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("salt identifier cannot be empty")
|
||||
}
|
||||
|
||||
salt, exists := ss.salts[id]
|
||||
if !exists {
|
||||
return fmt.Errorf("salt not found for identifier: %s", id)
|
||||
}
|
||||
|
||||
// Clear the salt from memory before removal
|
||||
salt.Clear()
|
||||
delete(ss.salts, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns all stored salt identifiers
|
||||
func (ss *SaltStore) List() []string {
|
||||
ids := make([]string, 0, len(ss.salts))
|
||||
for id := range ss.salts {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// Clear removes all salts and clears their memory
|
||||
func (ss *SaltStore) Clear() {
|
||||
for id, salt := range ss.salts {
|
||||
salt.Clear()
|
||||
delete(ss.salts, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the number of stored salts
|
||||
func (ss *SaltStore) Size() int {
|
||||
return len(ss.salts)
|
||||
}
|
||||
|
||||
// GenerateAndStore creates a new salt and stores it with the given identifier
|
||||
func (ss *SaltStore) GenerateAndStore(id string, size int) (*Salt, error) {
|
||||
salt, err := Generate(size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
||||
}
|
||||
|
||||
if err := ss.Store(id, salt); err != nil {
|
||||
salt.Clear() // Clean up on failure
|
||||
return nil, fmt.Errorf("failed to store salt: %w", err)
|
||||
}
|
||||
|
||||
return salt, nil
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
package salt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
wantErr bool
|
||||
}{
|
||||
{"minimum size", MinSaltSize, false},
|
||||
{"default size", DefaultSaltSize, false},
|
||||
{"large size", 512, false},
|
||||
{"maximum size", MaxSaltSize, false},
|
||||
{"too small", MinSaltSize - 1, true},
|
||||
{"too large", MaxSaltSize + 1, true},
|
||||
{"zero size", 0, true},
|
||||
{"negative size", -1, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
salt, err := Generate(tt.size)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Generate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr {
|
||||
if salt == nil {
|
||||
t.Error("Generate() returned nil salt without error")
|
||||
return
|
||||
}
|
||||
if salt.Size() != tt.size {
|
||||
t.Errorf("Generate() size = %d, want %d", salt.Size(), tt.size)
|
||||
}
|
||||
if salt.IsEmpty() {
|
||||
t.Error("Generate() returned empty salt")
|
||||
}
|
||||
|
||||
// Test that salt contains random data (not all zeros)
|
||||
saltBytes := salt.Bytes()
|
||||
allZeros := true
|
||||
for _, b := range saltBytes {
|
||||
if b != 0 {
|
||||
allZeros = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allZeros {
|
||||
t.Error("Generate() returned all-zero salt (likely not random)")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDefault(t *testing.T) {
|
||||
salt, err := GenerateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateDefault() error = %v", err)
|
||||
}
|
||||
|
||||
if salt.Size() != DefaultSaltSize {
|
||||
t.Errorf("GenerateDefault() size = %d, want %d", salt.Size(), DefaultSaltSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromBytes(t *testing.T) {
|
||||
validBytes := make([]byte, DefaultSaltSize)
|
||||
for i := range validBytes {
|
||||
validBytes[i] = byte(i)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid default size", validBytes, false},
|
||||
{"minimum size", make([]byte, MinSaltSize), false},
|
||||
{"large size", make([]byte, 512), false},
|
||||
{"too small", make([]byte, MinSaltSize-1), true},
|
||||
{"too large", make([]byte, MaxSaltSize+1), true},
|
||||
{"empty", []byte{}, true},
|
||||
{"nil", nil, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
salt, err := FromBytes(tt.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("FromBytes() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr {
|
||||
if salt == nil {
|
||||
t.Error("FromBytes() returned nil salt without error")
|
||||
return
|
||||
}
|
||||
if salt.Size() != len(tt.data) {
|
||||
t.Errorf("FromBytes() size = %d, want %d", salt.Size(), len(tt.data))
|
||||
}
|
||||
|
||||
// Verify data is copied correctly
|
||||
saltBytes := salt.Bytes()
|
||||
if !bytes.Equal(saltBytes, tt.data) {
|
||||
t.Error("FromBytes() data doesn't match input")
|
||||
}
|
||||
|
||||
// Verify external modification doesn't affect salt
|
||||
if len(tt.data) > 0 {
|
||||
originalValue := tt.data[0]
|
||||
tt.data[0] = ^tt.data[0] // Flip bits
|
||||
if salt.Bytes()[0] != originalValue {
|
||||
t.Error("FromBytes() salt was affected by external modification")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaltBytes(t *testing.T) {
|
||||
salt, err := GenerateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt: %v", err)
|
||||
}
|
||||
|
||||
bytes1 := salt.Bytes()
|
||||
bytes2 := salt.Bytes()
|
||||
|
||||
// Should return same data
|
||||
if !bytes.Equal(bytes1, bytes2) {
|
||||
t.Error("Bytes() returned different data on multiple calls")
|
||||
}
|
||||
|
||||
// Should be independent copies
|
||||
if &bytes1[0] == &bytes2[0] {
|
||||
t.Error("Bytes() returned same underlying array (not a copy)")
|
||||
}
|
||||
|
||||
// Modifying returned bytes shouldn't affect salt
|
||||
if len(bytes1) > 0 {
|
||||
originalValue := bytes1[0]
|
||||
bytes1[0] = ^bytes1[0]
|
||||
bytes3 := salt.Bytes()
|
||||
if bytes3[0] != originalValue {
|
||||
t.Error("External modification of Bytes() affected salt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaltEqual(t *testing.T) {
|
||||
salt1, err := Generate(DefaultSaltSize)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt1: %v", err)
|
||||
}
|
||||
|
||||
salt2, err := Generate(DefaultSaltSize)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt2: %v", err)
|
||||
}
|
||||
|
||||
// Same salt data
|
||||
salt3, err := FromBytes(salt1.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create salt3: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
salt1 *Salt
|
||||
salt2 *Salt
|
||||
expected bool
|
||||
}{
|
||||
{"same salt", salt1, salt1, true},
|
||||
{"equivalent salts", salt1, salt3, true},
|
||||
{"different salts", salt1, salt2, false},
|
||||
{"nil salts", nil, nil, true},
|
||||
{"one nil salt", salt1, nil, false},
|
||||
{"nil vs non-nil", nil, salt1, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.salt1.Equal(tt.salt2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Equal() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaltClear(t *testing.T) {
|
||||
salt, err := GenerateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt: %v", err)
|
||||
}
|
||||
|
||||
originalSize := salt.Size()
|
||||
if originalSize == 0 {
|
||||
t.Fatal("Salt size is zero before clear")
|
||||
}
|
||||
|
||||
salt.Clear()
|
||||
|
||||
if salt.Size() != 0 {
|
||||
t.Error("Salt size is not zero after clear")
|
||||
}
|
||||
|
||||
if !salt.IsEmpty() {
|
||||
t.Error("Salt is not empty after clear")
|
||||
}
|
||||
|
||||
bytes := salt.Bytes()
|
||||
if bytes != nil {
|
||||
t.Error("Bytes() should return nil after clear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaltStore(t *testing.T) {
|
||||
store := NewSaltStore()
|
||||
|
||||
// Test empty store
|
||||
if store.Size() != 0 {
|
||||
t.Error("New store should be empty")
|
||||
}
|
||||
|
||||
// Generate and store salt
|
||||
salt1, err := GenerateDefault()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt: %v", err)
|
||||
}
|
||||
|
||||
err = store.Store("test1", salt1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to store salt: %v", err)
|
||||
}
|
||||
|
||||
if store.Size() != 1 {
|
||||
t.Errorf("Store size = %d, want 1", store.Size())
|
||||
}
|
||||
|
||||
// Retrieve salt
|
||||
retrieved, err := store.Retrieve("test1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve salt: %v", err)
|
||||
}
|
||||
|
||||
if !salt1.Equal(retrieved) {
|
||||
t.Error("Retrieved salt doesn't match stored salt")
|
||||
}
|
||||
|
||||
// Test generate and store
|
||||
salt2, err := store.GenerateAndStore("test2", DefaultSaltSize)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate and store salt: %v", err)
|
||||
}
|
||||
|
||||
if store.Size() != 2 {
|
||||
t.Errorf("Store size = %d, want 2", store.Size())
|
||||
}
|
||||
|
||||
if salt2.Size() != DefaultSaltSize {
|
||||
t.Errorf("Generated salt size = %d, want %d", salt2.Size(), DefaultSaltSize)
|
||||
}
|
||||
|
||||
// Test list
|
||||
ids := store.List()
|
||||
if len(ids) != 2 {
|
||||
t.Errorf("List() returned %d ids, want 2", len(ids))
|
||||
}
|
||||
|
||||
foundTest1 := false
|
||||
foundTest2 := false
|
||||
for _, id := range ids {
|
||||
if id == "test1" {
|
||||
foundTest1 = true
|
||||
}
|
||||
if id == "test2" {
|
||||
foundTest2 = true
|
||||
}
|
||||
}
|
||||
if !foundTest1 || !foundTest2 {
|
||||
t.Error("List() doesn't contain expected IDs")
|
||||
}
|
||||
|
||||
// Test remove
|
||||
err = store.Remove("test1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to remove salt: %v", err)
|
||||
}
|
||||
|
||||
if store.Size() != 1 {
|
||||
t.Errorf("Store size = %d, want 1 after removal", store.Size())
|
||||
}
|
||||
|
||||
_, err = store.Retrieve("test1")
|
||||
if err == nil {
|
||||
t.Error("Should not be able to retrieve removed salt")
|
||||
}
|
||||
|
||||
// Test clear
|
||||
store.Clear()
|
||||
if store.Size() != 0 {
|
||||
t.Error("Store should be empty after clear")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaltStoreErrors(t *testing.T) {
|
||||
store := NewSaltStore()
|
||||
|
||||
// Test empty identifier errors
|
||||
err := store.Store("", nil)
|
||||
if err == nil {
|
||||
t.Error("Should error on empty identifier")
|
||||
}
|
||||
|
||||
_, err = store.Retrieve("")
|
||||
if err == nil {
|
||||
t.Error("Should error on empty identifier")
|
||||
}
|
||||
|
||||
err = store.Remove("")
|
||||
if err == nil {
|
||||
t.Error("Should error on empty identifier")
|
||||
}
|
||||
|
||||
// Test nil salt error
|
||||
err = store.Store("test", nil)
|
||||
if err == nil {
|
||||
t.Error("Should error on nil salt")
|
||||
}
|
||||
|
||||
// Test empty salt error
|
||||
emptySalt := &Salt{}
|
||||
err = store.Store("test", emptySalt)
|
||||
if err == nil {
|
||||
t.Error("Should error on empty salt")
|
||||
}
|
||||
|
||||
// Test retrieve non-existent
|
||||
_, err = store.Retrieve("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("Should error when retrieving non-existent salt")
|
||||
}
|
||||
|
||||
// Test remove non-existent
|
||||
err = store.Remove("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("Should error when removing non-existent salt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantTimeCompare(t *testing.T) {
|
||||
a := []byte{1, 2, 3, 4, 5}
|
||||
b := []byte{1, 2, 3, 4, 5}
|
||||
c := []byte{1, 2, 3, 4, 6}
|
||||
d := []byte{1, 2, 3, 4}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b []byte
|
||||
expected bool
|
||||
}{
|
||||
{"equal slices", a, b, true},
|
||||
{"different content", a, c, false},
|
||||
{"different length", a, d, false},
|
||||
{"empty slices", []byte{}, []byte{}, true},
|
||||
{"one empty", a, []byte{}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := constantTimeCompare(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("constantTimeCompare() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerate(b *testing.B) {
|
||||
sizes := []int{MinSaltSize, DefaultSaltSize, 512}
|
||||
|
||||
for _, size := range sizes {
|
||||
b.Run(fmt.Sprintf("%dB", size), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
salt, err := Generate(size)
|
||||
if err != nil {
|
||||
b.Fatalf("Generate error: %v", err)
|
||||
}
|
||||
salt.Clear() // Clean up
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSaltEqual(b *testing.B) {
|
||||
salt1, _ := GenerateDefault()
|
||||
salt2, _ := GenerateDefault()
|
||||
salt3, _ := FromBytes(salt1.Bytes())
|
||||
|
||||
b.Run("equal", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
salt1.Equal(salt3)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("different", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
salt1.Equal(salt2)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user