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
Zeroize(data []byte)?void
Overwrites every byte with 0, then runtime.KeepAlive. Returns immediately for a nil or empty slice.
voidZeroizeMultiple(slices ...[]byte)?void
Calls Zeroize on each argument. Convenient for a deferred wipe of several buffers.
voidZeroizeString(s *string)?void
Sets *s = "". Does not and cannot overwrite the string's bytes. Nil-safe.
voidSecureCompare(a, b []byte) bool?bool
Hand-rolled XOR-accumulate comparison. Returns false immediately when lengths differ, so length is not hidden.
boolSecureRandom(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.
errorSecureRandom 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()
}
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.
*SecureBytesFromBytes(data []byte)?*SecureBytes
Copies data into a new instance. Empty input yields a nil-data instance with no finalizer.
*SecureBytesBytes()?[]byte
Returns a fresh COPY of the contents — a new secret you are now responsible for zeroizing. Returns nil once cleared.
[]byteCopyTo(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.
errorSize?int
Length of the held data; 0 after Clear.
intIsEmpty?bool
True if the data is nil or zero-length.
boolClear?void
Zeroizes, drops the data, marks finalized, and unregisters the finalizer. Safe to call twice.
voidSecureString
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.
NewSecureBuffer(capacity int)?*SecureBuffer
Allocates make([]byte, 0, capacity). A capacity <= 0 is silently replaced with 1024.
*SecureBufferWrite(data []byte)?error
Appends. Returns a 'buffer overflow' error instead of growing when len+len(data) would exceed capacity.
errorRead()?[]byte
Returns a copy of the current contents.
[]byteReset()?void
Zeroizes the entire backing array (up to cap) and truncates length to 0, retaining capacity. No-op when length is already 0.
voidClear()?void
Zeroizes the entire backing array, sets it to nil, and unregisters the finalizer.
voidSize?int
Current length.
intCapacity?int
Backing-array capacity; 0 after Clear.
intsalt
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
}
Bytes()?[]byte
Returns a copy. nil if the Salt is nil or cleared.
[]byteSize()?int
Length in bytes; 0 when nil or cleared.
intString()?string
Redacted form: "Salt{size=32}" or "Salt{<nil>}". Never exposes the value.
stringEqual(other *Salt)?bool
Constant-time comparison over equal-length values; returns false on a length mismatch. Nil-safe (nil equals nil).
boolClear()?void
Zeroizes the value and sets it to nil. Nil-safe.
voidIsEmpty()?bool
True if the Salt is nil, has nil value, or is zero-length.
boolEvery *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.
MinLengthint
Minimum length, compared against len(password) in BYTES.
int12MaxLengthint
Maximum length in bytes.
int128RequireUppercase?bool
Require at least one unicode.IsUpper rune.
booltrueRequireLowercase?bool
Require at least one unicode.IsLower rune.
booltrueRequireDigits?bool
Require at least one unicode.IsDigit rune.
booltrueRequireSpecial?bool
Require at least one unicode.IsPunct or unicode.IsSymbol rune. Whitespace does NOT count as special.
booltrueMinEntropyfloat64
Minimum estimated entropy in bits, from the package's own heuristic (see caveat).
float6450.0Those 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.