---
title: Secret Hygiene
description: The secure, salt, password, and subtle/random helpers — zeroization, salt management, password policy, and randomness, with an honest account of what each actually guarantees.
sidebar:
  order: 5
  icon: eye-off
---

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

:::warning[This package overwrites memory. It does not lock it.]
Read `secure/memory.go` and you will find `crypto/rand`, `fmt`, `runtime`, and `sync` — and nothing else. There is **no** `mlock`, no `munlock`, no `madvise`, no `syscall` import, and no build-tagged platform file. Concretely:

- Secrets held by this package **can be paged to swap** or captured in a core dump or hibernation image. If that matters, disable swap for the process, or lock pages yourself outside this library.
- `Zeroize` is a plain `for i := range data { data[i] = 0 }` followed by `runtime.KeepAlive(data)`. The comment claims the loop "prevent[s] compiler optimizations"; in practice a loop writing to a heap slice that is later kept alive is not something the current Go compiler elides, but this is a convention, not a language guarantee. Go has no `explicit_bzero`.
- **The Go runtime may already have copied your secret.** A growing slice, an `append`, a map rehash, or a moving GC leaves stale copies that `Zeroize` cannot reach, because it only sees the slice header you hand it. Zeroize the *original* buffer as early as possible and avoid copying secrets into intermediate values.

Treat zeroization as defence in depth that shortens a secret's lifetime, not as a boundary that guarantees erasure.
:::

:::danger[ZeroizeString is a no-op on the actual bytes]
`ZeroizeString(s *string)` does exactly one thing: `*s = ""`. Its own comment says "(limited effectiveness)". Go strings are immutable and their backing bytes are not writable through the language, so the original characters remain in the heap until the GC collects them — and if the string was interned, is a compile-time constant, or is shared with any other variable, they remain reachable and unchanged. `SecureString.Clear()` calls this function, so `SecureString` inherits the same limitation.

The fix is not a better `ZeroizeString`; it is to never put a secret in a `string`. Read passwords and keys into `[]byte`, pass `[]byte` all the way down (`argon2.DeriveKey`, `password.Validate`, and `aead.Encrypt` all take `[]byte`), and `Zeroize` that.
:::

### Free functions

| Prop | Type | Default | Description |
| - | - | - | - |
| `Zeroize(data []byte)?` | `void` | - | Overwrites every byte with 0, then runtime.KeepAlive. Returns immediately for a nil or empty slice. |
| `ZeroizeMultiple(slices ...[]byte)?` | `void` | - | Calls Zeroize on each argument. Convenient for a deferred wipe of several buffers. |
| `ZeroizeString(s *string)?` | `void` | - | Sets *s = "". Does not and cannot overwrite the string's bytes. Nil-safe. |
| `SecureCompare(a, b []byte) bool?` | `bool` | - | Hand-rolled XOR-accumulate comparison. Returns false immediately when lengths differ, so length is not hidden. |
| `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. |

`SecureRandom` is the randomness call to prefer in this library: it reports failure instead of panicking, unlike [`subtle/random`](#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`):

```go secure_bytes.go
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()
}
```

| Prop | Type | Default | Description |
| - | - | - | - |
| `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. |
| `FromBytes(data []byte)?` | `*SecureBytes` | - | Copies data into a new instance. Empty input yields a nil-data instance with no finalizer. |
| `Bytes()?` | `[]byte` | - | Returns a fresh COPY of the contents — a new secret you are now responsible for zeroizing. Returns nil once cleared. |
| `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. |
| `Size?` | `int` | - | Length of the held data; 0 after Clear. |
| `IsEmpty?` | `bool` | - | True if the data is nil or zero-length. |
| `Clear?` | `void` | - | Zeroizes, drops the data, marks finalized, and unregisters the finalizer. Safe to call twice. |

:::warning[Bytes() manufactures new copies of your secret]
Every `Bytes()` call allocates and returns a fresh slice — that is what makes the type safe against external mutation, and it is also what makes it leaky. Each returned slice is an independent copy that `Clear()` will never touch. Call `Bytes()` once, use it, and `Zeroize` the result yourself.

Relying on the finalizer is worse still: `runtime.SetFinalizer` runs at the GC's discretion and is not guaranteed to run at all before the process exits. Always `defer sb.Clear()`.
:::

### 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.

| Prop | Type | Default | Description |
| - | - | - | - |
| `NewSecureBuffer(capacity int)?` | `*SecureBuffer` | - | Allocates make([]byte, 0, capacity). A capacity <= 0 is silently replaced with 1024. |
| `Write(data []byte)?` | `error` | - | Appends. Returns a 'buffer overflow' error instead of growing when len+len(data) would exceed capacity. |
| `Read()?` | `[]byte` | - | Returns a copy of the current contents. |
| `Reset()?` | `void` | - | Zeroizes the entire backing array (up to cap) and truncates length to 0, retaining capacity. No-op when length is already 0. |
| `Clear()?` | `void` | - | Zeroizes the entire backing array, sets it to nil, and unregisters the finalizer. |
| `Size?` | `int` | - | Current length. |
| `Capacity?` | `int` | - | Backing-array capacity; 0 after Clear. |

:::note[SecureBuffer never grows, and Reset skips an empty buffer]
`Write` fails rather than reallocating — deliberate, since a growing slice would leave an un-wipeable copy behind, but it means you must size the buffer up front. Also note `Reset()` is guarded by `if len(sb.buffer) > 0`, so it does nothing when the length is already zero; after a `Reset` the capacity region stays wiped, but do not depend on `Reset` as a general "scrub this" call. After `Clear()`, capacity is 0 and every subsequent `Write` fails with an overflow error.
:::

## 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.

```go salts.go
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
}
```

| Prop | Type | Default | Description |
| - | - | - | - |
| `Bytes()?` | `[]byte` | - | Returns a copy. nil if the Salt is nil or cleared. |
| `Size()?` | `int` | - | Length in bytes; 0 when nil or cleared. |
| `String()?` | `string` | - | Redacted form: "Salt{size=32}" or "Salt{<nil>}". Never exposes the value. |
| `Equal(other *Salt)?` | `bool` | - | Constant-time comparison over equal-length values; returns false on a length mismatch. Nil-safe (nil equals nil). |
| `Clear()?` | `void` | - | Zeroizes the value and sets it to nil. Nil-safe. |
| `IsEmpty()?` | `bool` | - | True if the Salt is nil, has nil value, or is zero-length. |

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`.

:::danger[SaltStore is in-memory only and NOT concurrency-safe]
Two independent facts, both from `salt.go`:

1. The struct is exactly `struct { salts map[string]*Salt }`. There is **no mutex** — no `sync.Mutex`, no `sync.RWMutex`, no `sync.Map`. Concurrent `Store` and `Retrieve` from different goroutines is a data race on a Go map, and concurrent writes will crash the process with `fatal error: concurrent map writes`. Wrap it in your own lock or confine it to one goroutine.
2. There is no persistence, no encryption at rest, and no export/import. Everything lives in the process heap and is gone on restart. Salts do not need to be secret, but they do need to *survive* — a lost salt means an unverifiable password hash and an underivable key. Persist salts alongside the records they belong to (or use `argon2.HashPassword`, which embeds the salt in the encoded string) and treat `SaltStore` as a request-scoped cache at most.
:::

## 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`](/symmetric/key-derivation#argon2-password-stretching).

| Prop | Type | Default | Description |
| - | - | - | - |
| `MinLength` | `int` | `12` | Minimum length, compared against len(password) in BYTES. |
| `MaxLength` | `int` | `128` | Maximum length in bytes. |
| `RequireUppercase?` | `bool` | `true` | Require at least one unicode.IsUpper rune. |
| `RequireLowercase?` | `bool` | `true` | Require at least one unicode.IsLower rune. |
| `RequireDigits?` | `bool` | `true` | Require at least one unicode.IsDigit rune. |
| `RequireSpecial?` | `bool` | `true` | Require at least one unicode.IsPunct or unicode.IsSymbol rune. Whitespace does NOT count as special. |
| `MinEntropy` | `float64` | `50.0` | Minimum estimated entropy in bits, from the package's own heuristic (see caveat). |

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

```go policy.go
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`.

:::warning[There is no blocklist, dictionary, or breach check]
`Validate` enforces length and character classes only. `Passw0rd123!` passes every default rule — 12 bytes, all four classes, 84 "bits" by the internal estimator. If you care about guessability rather than shape, add a check against a common-password list or a breached-credential API. This package cannot tell you a password is bad, only that it is short or monotonous.
:::

:::warning[MinEntropy is a length heuristic, and with the default MinLength it is nearly vacuous]
`calculateEntropy` detects which of four character classes appear, sums a pool size (26 lower + 26 upper + 10 digit + 32 special = 94 at most), then computes bits-per-character by counting the bits in that integer — `floor(log2(pool)) + 1`, so 7 for the full pool — and returns `len(password) * bitsPerChar`. It is a per-character constant multiplied by a byte count. It does not measure repetition, patterns, or dictionary membership: `aaaaaaaaaaaa` scores 60 "bits". Since 12 characters clears the 50-bit default even in the lowest-scoring case, the entropy gate essentially never fires beyond what `MinLength` already rejected. Do not present its number to users as a strength meter.
:::

:::warning[Length limits are counted in bytes, not characters]
`Validate` compares `len(password)`, the byte length. A 12-character password made of multi-byte runes (accents, CJK, emoji) can be 24–48 bytes and may trip `MaxLength`, while `MinLength: 12` is satisfied by as few as 3 emoji. The character-class loop, in contrast, iterates properly over runes via `for _, ch := range string(password)`. If your users type non-ASCII, either raise `MaxLength` or count runes before calling.
:::

:::note[Normalize before validating and before hashing]
There is no Unicode normalization anywhere in this package or in `argon2`. The same typed password can produce different byte sequences (NFC vs NFD) depending on the client's input method, and a hash derived from one will not verify the other. If you accept non-ASCII passwords, normalize to a fixed form (NFKC is the usual choice) at the edge, before both `Validate` and `DeriveKey`.
:::

## 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))` |

:::danger[These functions panic on randomness failure]
Neither returns an error. `GetRandomBytes` handles a failed `rand.Read` with `panic(err)`, annotated `// out of randomness, should never happen`. `GetRandomUint32` inherits that panic.

On Linux with a modern kernel, `crypto/rand.Read` failing is genuinely close to impossible, so the assumption usually holds — but "usually" is the operative word: a panic in a library function is an unrecoverable crash of whichever goroutine calls it, and you cannot handle it at the call site. In any long-lived service, prefer `secure.SecureRandom(buf)`, which returns a wrapped error, or call `crypto/rand.Read` directly. Reserve `subtle/random` for tests and for code paths where a crash is an acceptable response to a broken CSPRNG.
:::

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`** |

:::warning[Prefer the crypto/subtle-backed comparison]
`argon2.CompareHashes` delegates to `crypto/subtle.ConstantTimeCompare`, which the Go team maintains and documents as constant-time. `secure.SecureCompare`, `password.SecureCompare`, and `salt`'s `constantTimeCompare` are three copies of the same hand-written XOR-accumulate loop. The loop is the textbook shape and is very likely constant-time as compiled today, but it carries no guarantee from the compiler and gets no attention from anyone tracking Go's optimizer. There is no reason to prefer a hand-rolled copy over the standard library's.

All four variants short-circuit on a length mismatch, so **none** of them hides the length of the secret. That is fine for fixed-width comparisons (32-byte keys, 16-byte tags) and wrong for variable-length inputs; if length is sensitive, hash both sides to a fixed width first and compare the digests.
:::

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`.

## Related

<CardGroup cols={2}>
  <Card title="Key derivation" href="/symmetric/key-derivation" icon="key-round">
    Argon2id presets and the HKDF/X25519 layer that consume these salts and randomness.
  </Card>
  <Card title="Randomized AEAD" href="/symmetric/aead" icon="lock-keyhole">
    AES-256-GCM — the consumer of the 32-byte keys you are trying to keep short-lived.
  </Card>
</CardGroup>
