slackhq/nebula · error

no cipher state available to encrypt

Error message

no cipher state available to encrypt

What it means

Identical guard to the AES-GCM wrapper but for ChaCha20-Poly1305: if the CipherStateChaChaPoly receiver is nil there is no AEAD to encrypt with, so EncryptDanger returns this error rather than panicking.

Source

Thrown at noiseutil/chachapoly.go:25

	"github.com/flynn/noise"
)

// CipherStateChaChaPoly is the data-plane wrapper for the ChaCha20-Poly1305 AEAD cipher.
// ChaCha20-Poly1305 uses little-endian nonce encoding per the Noise spec.
type CipherStateChaChaPoly struct {
	c cipher.AEAD
}

// NewCipherStateChaChaPoly extracts the underlying AEAD from the post-handshake noise.CipherState.
// The caller is responsible for ensuring the noise cipher is actually ChaCha20-Poly1305.
func NewCipherStateChaChaPoly(s *noise.CipherState) *CipherStateChaChaPoly {
	return &CipherStateChaChaPoly{c: s.Cipher().(cipher.AEAD)}
}

func (s *CipherStateChaChaPoly) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) {
	if s == nil {
		return nil, errors.New("no cipher state available to encrypt")
	}
	if n >= RejectAfterMessages {
		return nil, ErrMessageCounterExhausted
	}
	nb[0] = 0
	nb[1] = 0
	nb[2] = 0
	nb[3] = 0
	binary.LittleEndian.PutUint64(nb[4:], n)
	return s.c.Seal(out, nb, plaintext, ad), nil
}

func (s *CipherStateChaChaPoly) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) {
	if s == nil {
		return []byte{}, nil
	}
	nb[0] = 0
	nb[1] = 0

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Verify the handshake produced a valid *noise.CipherState before wrapping with NewCipherStateChaChaPoly.
  2. Guard the send path with a nil check on the ChaChaPoly cipher state.
  3. If using CipherChoice_ChaChaPoly, confirm the peer negotiated the same cipher choice so a state is actually established.

Example fix

// before
if s == nil { return nil, errors.New("no cipher state available to encrypt") }
// after: caller-side
if chacha == nil { rehandshake(); }
Defensive patterns

Strategy: type-guard

Validate before calling

if cs == nil {
    return errors.New("chachapoly cipher state not initialized")
}

Type guard

func hasCipherState(cs *noiseutil.CipherStateChaChaPoly) bool { return cs != nil }

Try / catch

out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)
if err != nil {
    // nil state or counter exhaustion: force re-handshake
}

Prevention

When it happens

Trigger: Calling EncryptDanger on a nil *CipherStateChaChaPoly, typically produced by NewCipherStateChaChaPoly(nil) or an unset post-handshake state; covered by TestCipherStateNilSafety.

Common situations: Failed or skipped noise handshake leaving the send-side cipher unset, storing cipher states in a map and hitting a missing key, or tests constructing the wrapper directly without an underlying state.

Related errors


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