slackhq/nebula · error
salt must be set in argon2Parameters
Error message
salt must be set in argon2Parameters
What it means
deriveKey requires params.salt to be non-nil and at least 16 bytes. The public aes256DeriveKey auto-generates a random 32-byte salt when nil, so reaching this error means deriveKey was called directly (or via a path bypassing aes256DeriveKey) with no salt configured. A salt is required for Argon2id key derivation.
Source
Thrown at cert/crypto.go:135
}
// keySize of 32 bytes will result in AES-256 encryption
key, err := deriveKey(passphrase, 32, params)
if err != nil {
return nil, err
}
return key, nil
}
// Derives a key from a passphrase using Argon2id
func deriveKey(passphrase []byte, keySize uint32, params *Argon2Parameters) ([]byte, error) {
if params.version != argon2.Version {
return nil, fmt.Errorf("incompatible Argon2 version: %d", params.version)
}
if params.salt == nil {
return nil, fmt.Errorf("salt must be set in argon2Parameters")
} else if len(params.salt) < 16 {
return nil, fmt.Errorf("salt must be at least 128 bits")
}
key := argon2.IDKey(passphrase, params.salt, params.Iterations, params.Memory, params.Parallelism, keySize)
return key, nil
}
// Prepends nonce to ciphertext
func joinNonceCiphertext(nonce []byte, ciphertext []byte) []byte {
return append(nonce, ciphertext...)
}
// Splits nonce from ciphertext
func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
if len(blob) <= nonceSize {
return nil, nil, fmt.Errorf("invalid ciphertext blob - blob shorter than nonce length")View on GitHub (pinned to dd8f660c0a)
Solutions
- Set a random >=16-byte salt: params.salt = make([]byte, 32); rand.Read(params.salt).
- Prefer the public flow (NewArgon2Parameters + aes256Encrypt/aes256Decrypt) which generates the salt automatically.
- If decrypting, ensure the salt stored in the metadata blob was not stripped or zeroed.
Example fix
// before params := cert.NewArgon2Parameters(9*1024, 1, 4) params.Salt = nil // derive fails downstream in custom KDF call // after salt := make([]byte, 32) io.ReadFull(rand.Reader, salt) params.Salt = salt
Defensive patterns
Strategy: validation
Validate before calling
params := cert.NewArgon2Parameters(9*1024, 1, 4)
// for decrypt flows, ensure salt from metadata is >= 16 bytes before use
if len(storedSalt) > 0 && len(storedSalt) < 16 {
return fmt.Errorf("stored salt must be at least 16 bytes")
} Type guard
func validSalt(salt []byte) bool {
return len(salt) >= 16
} Try / catch
if err != nil && strings.Contains(err.Error(), "salt must be set") {
return fmt.Errorf("Argon2 parameters missing salt; use NewArgon2Parameters + the public encrypt/decrypt API")
} Prevention
- Use the public encrypt/decrypt API which auto-generates a random salt.
- Never pass nil or empty salts to KDF code directly.
- Preserve the salt bytes stored in encrypted metadata when moving files between systems.
When it happens
Trigger: Calling deriveKey (unexported) or constructing Argon2Parameters and invoking a KDF path that skips aes256DeriveKey's nil-salt random generation, e.g. custom code or a salt that is a non-nil empty slice would hit the <16 bytes branch instead.
Common situations: Forks/custom crypto code that call deriveKey directly; tests building Argon2Parameters manually; deserializing parameters from data with a zero-length salt field.
Related errors
- incompatible Argon2 version: %d
- salt must be at least 128 bits
- no cipher state available to encrypt
- no cipher state available to encrypt
- invalid AES-256 key length (%d) - cowardly refusing to encry
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/ae547295ae2d9e33.
Report an issue: GitHub.