Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

Secret Hygiene

The secure, salt, password, and subtle/random helpers — zeroization, salt management, password policy, and randomness, with an honest account of what each actually guarantees.

Four small packages that surround the cryptography rather than performing it: wiping key material after use, generating and tracking salts, enforcing a password policy at signup, and getting random bytes. None of them is load-bearing for confidentiality — a correct aead call with a correctly derived key is secure whether or not you zeroize afterwards — but they are the difference between a key living for microseconds and living until the process exits into a core dump.

They are also the least polished corner of this library. Several helpers are duplicated across packages, one guarantee is weaker than its name suggests, and one type is not safe for concurrent use. Everything below states what the code does.

secure — zeroization and wrapped secrets

github.com/sonr-io/crypto/secure provides free functions for wiping and comparing byte slices, plus three container types that wipe themselves.

The guarantee, precisely

Free functions

PropType
Zeroize(data []byte)?void

Overwrites every byte with 0, then runtime.KeepAlive. Returns immediately for a nil or empty slice.

Typevoid
ZeroizeMultiple(slices ...[]byte)?void

Calls Zeroize on each argument. Convenient for a deferred wipe of several buffers.

Typevoid
ZeroizeString(s *string)?void

Sets *s = "". Does not and cannot overwrite the string's bytes. Nil-safe.

Typevoid
SecureCompare(a, b []byte) bool?bool

Hand-rolled XOR-accumulate comparison. Returns false immediately when lengths differ, so length is not hidden.

Typebool
SecureRandom(data []byte) error?error

Fills data from crypto/rand.Read. Returns nil for an empty slice. Wraps any read failure as an error rather than panicking.

Typeerror

SecureRandom is the randomness call to prefer in this library: it reports failure instead of panicking, unlike subtle/random.

SecureBytes

A mutex-guarded byte buffer with a runtime.SetFinalizer that wipes it if you forget to. Grounded in secure/memory_test.go (TestSecureBytes):

package main

import (
	"fmt"

	"github.com/sonr-io/crypto/secure"
)

func main() {
	// Allocate a zeroed 32-byte secret holder.
	sb := secure.NewSecureBytes(32)
	defer sb.Clear() // idempotent; also removes the finalizer

	// Bytes() hands back a copy, so writes must go through CopyTo.
	scratch := make([]byte, sb.Size())
	if err := secure.SecureRandom(scratch); err != nil {
		panic(err)
	}
	if err := sb.CopyTo(scratch); err != nil {
		panic(err)
	}
	secure.Zeroize(scratch) // wipe the intermediate immediately

	fmt.Println(sb.Size(), sb.IsEmpty()) // 32 false

	// Wrapping existing material copies it — the source stays independent.
	raw := []byte{1, 2, 3, 4, 5}
	wrapped := secure.FromBytes(raw)
	secure.Zeroize(raw) // wiping the original does not affect `wrapped`

	out := wrapped.Bytes() // a fresh copy: your responsibility now
	fmt.Println(out)       // [1 2 3 4 5]
	secure.Zeroize(out)
	wrapped.Clear()
}
PropType
NewSecureBytes(size int)?*SecureBytes

Allocates a zeroed buffer of `size` bytes and registers a finalizer. size <= 0 yields a nil-data instance with NO finalizer.

Type*SecureBytes
FromBytes(data []byte)?*SecureBytes

Copies data into a new instance. Empty input yields a nil-data instance with no finalizer.

Type*SecureBytes
Bytes()?[]byte

Returns a fresh COPY of the contents — a new secret you are now responsible for zeroizing. Returns nil once cleared.

Type[]byte
CopyTo(data []byte)?error

Zeroizes the buffer then copies data in. Errors if finalized, if the buffer is nil, or if len(data) exceeds the buffer.

Typeerror
Size?int

Length of the held data; 0 after Clear.

Typeint
IsEmpty?bool

True if the data is nil or zero-length.

Typebool
Clear?void

Zeroizes, drops the data, marks finalized, and unregisters the finalizer. Safe to call twice.

Typevoid

SecureString

NewSecureString(s string) wraps a string; String() returns the value (or "" once cleared), IsEmpty() reports finalized-or-empty, and Clear() calls ZeroizeString and marks it finalized. Given the ZeroizeString limitation above, this type buys you a “cleared” flag and a mutex, not erasure. Prefer SecureBytes.

SecureBuffer

A fixed-capacity append-only buffer for assembling sensitive data.

PropType
NewSecureBuffer(capacity int)?*SecureBuffer

Allocates make([]byte, 0, capacity). A capacity <= 0 is silently replaced with 1024.

Type*SecureBuffer
Write(data []byte)?error

Appends. Returns a 'buffer overflow' error instead of growing when len+len(data) would exceed capacity.

Typeerror
Read()?[]byte

Returns a copy of the current contents.

Type[]byte
Reset()?void

Zeroizes the entire backing array (up to cap) and truncates length to 0, retaining capacity. No-op when length is already 0.

Typevoid
Clear()?void

Zeroizes the entire backing array, sets it to nil, and unregisters the finalizer.

Typevoid
Size?int

Current length.

Typeint
Capacity?int

Backing-array capacity; 0 after Clear.

Typeint

salt

github.com/sonr-io/crypto/salt wraps salt bytes in a type that redacts itself in logs, compares in constant time, and can wipe itself — plus an in-memory keyed store.

Constant Value Meaning
salt.DefaultSaltSize 32 Recommended size, 256 bits — matches argon2.DefaultConfig().SaltLength
salt.MinSaltSize 16 Hard floor, 128 bits. Anything smaller is rejected
salt.MaxSaltSize 1024 Hard ceiling, to prevent resource exhaustion

Generate(size) errors outside [16, 1024]; GenerateDefault() is Generate(32); FromBytes(data) applies the same bounds and copies the input so later mutation of your slice cannot change the salt.

package main

import (
	"fmt"

	"github.com/sonr-io/crypto/argon2"
	"github.com/sonr-io/crypto/salt"
)

func main() {
	s, err := salt.GenerateDefault() // 32 bytes
	if err != nil {
		panic(err)
	}
	defer s.Clear()

	fmt.Println(s.Size())      // 32
	fmt.Println(s.String())    // Salt{size=32} — value is never printed
	fmt.Println(s.IsEmpty())   // false

	key := argon2.New(argon2.DefaultConfig()).DeriveKey([]byte("pw"), s.Bytes())
	fmt.Println(len(key))      // 32

	// Round-tripping a persisted salt.
	restored, err := salt.FromBytes(s.Bytes())
	if err != nil {
		panic(err)
	}
	fmt.Println(s.Equal(restored)) // true, compared in constant time
}
PropType
Bytes()?[]byte

Returns a copy. nil if the Salt is nil or cleared.

Type[]byte
Size()?int

Length in bytes; 0 when nil or cleared.

Typeint
String()?string

Redacted form: "Salt{size=32}" or "Salt{<nil>}". Never exposes the value.

Typestring
Equal(other *Salt)?bool

Constant-time comparison over equal-length values; returns false on a length mismatch. Nil-safe (nil equals nil).

Typebool
Clear()?void

Zeroizes the value and sets it to nil. Nil-safe.

Typevoid
IsEmpty()?bool

True if the Salt is nil, has nil value, or is zero-length.

Typebool

Every *Salt method is nil-receiver safe, which is unusual and worth knowing: a nil salt reports Size() == 0 and IsEmpty() == true rather than panicking.

SaltStore

NewSaltStore() returns a keyed collection with Store(id, salt), Retrieve(id), GenerateAndStore(id, size), Remove(id), List(), Size(), and Clear(). Store and Retrieve both copy, so the store never shares a backing array with your code, and Remove/Clear zeroize before deleting. An empty id is an error, as is storing a nil-or-empty salt or retrieving/removing an unknown id.

password

github.com/sonr-io/crypto/password is a policy checker, not a hasher — it never touches Argon2. Feed a candidate password through Validate at signup or change-password time, then hand it to argon2.

PropType
MinLengthint

Minimum length, compared against len(password) in BYTES.

Typeint
Default12
MaxLengthint

Maximum length in bytes.

Typeint
Default128
RequireUppercase?bool

Require at least one unicode.IsUpper rune.

Typebool
Defaulttrue
RequireLowercase?bool

Require at least one unicode.IsLower rune.

Typebool
Defaulttrue
RequireDigits?bool

Require at least one unicode.IsDigit rune.

Typebool
Defaulttrue
RequireSpecial?bool

Require at least one unicode.IsPunct or unicode.IsSymbol rune. Whitespace does NOT count as special.

Typebool
Defaulttrue
MinEntropyfloat64

Minimum estimated entropy in bits, from the package's own heuristic (see caveat).

Typefloat64
Default50.0

Those are the literal values in DefaultPasswordConfig(). NewValidator(nil) uses them.

package main

import (
	"fmt"

	"github.com/sonr-io/crypto/password"
)

func main() {
	v := password.NewValidator(nil) // DefaultPasswordConfig()

	// Rejected: 7 bytes < MinLength 12.
	fmt.Println(v.Validate([]byte("Short1!")))
	// password must be at least 12 characters

	// Rejected: no uppercase.
	fmt.Println(v.Validate([]byte("longenoughpassword123!")))
	// password must contain at least one uppercase letter

	// Accepted.
	fmt.Println(v.Validate([]byte("ValidPassword123!"))) // <nil>

	// Loosen the policy explicitly rather than editing the default.
	relaxed := password.NewValidator(&password.PasswordConfig{
		MinLength:        8,
		MaxLength:        64,
		RequireUppercase: false,
		RequireLowercase: true,
		RequireDigits:    true,
		RequireSpecial:   false,
		MinEntropy:       30.0,
	})
	fmt.Println(relaxed.Validate([]byte("simple123"))) // <nil>
}

Validate returns the first violated rule as a fmt.Errorf string, checked in order: min length, max length, uppercase, lowercase, digit, special, entropy. There is no aggregated result, so a UI that wants to show every failure must call it repeatedly with narrowed configs.

The three loose helpers are unrelated to validation: GenerateSalt(size) returns size random bytes and errors below 16; SecureCompare(a, b) is the same XOR loop as secure.SecureCompare; ZeroBytes(b) is the same wipe loop as secure.Zeroize minus the runtime.KeepAlive.

subtle random

github.com/sonr-io/crypto/subtle/random is nine lines of code with two functions:

Function Behaviour
GetRandomBytes(n uint32) []byte Allocates n bytes and fills them from crypto/rand.Read
GetRandomUint32() uint32 binary.BigEndian.Uint32(GetRandomBytes(4))

Inside this repository, random is used by test code (for example the AES-SIV tests) rather than by production paths — which is roughly the right scope for a panicking API.

Duplicated helpers

The same two primitives are implemented three and four times over. They are not identical, and it matters which you call.

Primitive Implementations Prefer
Constant-time compare secure.SecureCompare, password.SecureCompare, salt’s internal constantTimeCompare (via Salt.Equal), argon2.CompareHashes argon2.CompareHashes
Zero a byte slice secure.Zeroize, password.ZeroBytes, Salt.Clear secure.Zeroize
Generate a salt salt.Generate, argon2.(*KDF).GenerateSalt, password.GenerateSalt salt.Generate / GenerateDefault
Random bytes secure.SecureRandom, random.GetRandomBytes secure.SecureRandom

For salt generation, salt.Generate is the strictest: it enforces the 16-byte floor and a 1024-byte ceiling. password.GenerateSalt enforces only the 16-byte floor, and (*KDF).GenerateSalt enforces nothing beyond using the configured SaltLength.

Last updated on September 2, 2026

Was this page helpful?