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

  1. 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
  2. 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
  3. 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

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


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/d00fa5c5ec69ba90. Report an issue: GitHub.