slackhq/nebula · error
salt must be at least 128 bits
Error message
salt must be at least 128 bits
What it means
deriveKey rejects Argon2 parameters whose salt is shorter than 16 bytes (128 bits). Argon2 requires a sufficiently long salt to prevent precomputation/rainbow-table attacks, so this library enforces the NIST-recommended 128-bit minimum before calling argon2.IDKey. If aes256DeriveKey is given a passphrase-encryption setup with a missing/short salt, key derivation aborts.
Source
Thrown at cert/crypto.go:137
// 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
- Generate a 16-byte (or larger) cryptographically random salt, e.g. salt := make([]byte, 16); rand.Read(salt), and set params.Salt before calling the encrypt/derive API
- Check where the salt was produced - if it came from serialized data, re-encrypt with a proper-length salt rather than padding the short one
- Verify you are not accidentally passing a truncated slice (e.g. salt[:8]) when assembling argon2Parameters
Example fix
// before
params := &cert.Argon2Parameters{Salt: []byte("shortsalt"), ...}
key, err := cert.DeriveKey(passphrase, params)
// after
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil { return err }
params := &cert.Argon2Parameters{Salt: salt, ...}
key, err := cert.DeriveKey(passphrase, params) Defensive patterns
Strategy: validation
Validate before calling
if params.Salt == nil || len(params.Salt) < 16 { return fmt.Errorf("salt must be >= 16 bytes, got %d", len(params.Salt)) }
// then call aes256DeriveKey / EncryptAndMarshalSigningPrivateKey Type guard
func hasValidSalt(p *cert.Argon2Parameters) bool { return p != nil && len(p.Salt) >= 16 } Try / catch
key, err := cert.DeriveKey(pass, params)
if err != nil {
if strings.Contains(err.Error(), "salt must be") { /* regenerate salt */ }
return err
} Prevention
- Always generate salts with crypto/rand at >= 16 bytes
- Never hardcode or reuse salts; generate a fresh one per encryption
- Validate Argon2Parameters in a constructor/helper before use
When it happens
Trigger: Calling aes256DeriveKey (via EncryptAndMarshalSigningPrivateKey or DecryptAndUnmarshalSigningPrivateKey) with Argon2Parameters whose Salt field is a non-nil byte slice of length 1-15. A nil salt produces the distinct 'salt must be set' error first; this error fires only when salt exists but len(salt) < 16.
Common situations: Hand-crafting NebulaEncryptedData or Argon2Parameters structs in tests/tools; truncating a salt from an older or external encryption tool; copying a hardcoded example salt that is too short; migrating certs produced by non-nebula tooling with 8-byte salts.
Related errors
- salt must be set in argon2Parameters
- nil byte array
- encoded Argon2Parameters was nil
- Argon2Parameters Version must be at least %d and no more tha
- Argon2Parameters Memory must be be greater than 0 and no mor
AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03).
Data as JSON: /api/errors/d00fa5c5ec69ba90.
Report an issue: GitHub.