slackhq/nebula · error

no cipher state available to encrypt

Error message

no cipher state available to encrypt

What it means

The AES-GCM CipherState wrapper in noiseutil encrypts data-plane packets. If the wrapper itself is a nil pointer, there is no underlying cipher.AEAD to seal with, so EncryptDanger fails fast instead of panicking on a nil dereference.

Source

Thrown at noiseutil/aesgcm.go:26

	"github.com/flynn/noise"
)

// CipherStateAESGCM is the data-plane wrapper for the AES-GCM AEAD cipher.
// AES-GCM uses big-endian nonce encoding per the Noise spec.
type CipherStateAESGCM struct {
	c cipher.AEAD
}

// NewCipherStateAESGCM extracts the underlying AEAD from the post-handshake noise.CipherState.
// The caller is responsible for ensuring the noise cipher is actually AES-GCM,
// otherwise the type assertion still succeeds but the nonce endianness will be wrong on the wire.
func NewCipherStateAESGCM(s *noise.CipherState) *CipherStateAESGCM {
	return &CipherStateAESGCM{c: s.Cipher().(cipher.AEAD)}
}

func (s *CipherStateAESGCM) 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.BigEndian.PutUint64(nb[4:], n)
	return s.c.Seal(out, nb, plaintext, ad), nil
}

func (s *CipherStateAESGCM) 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. Check that NewCipherStateAESGCM receives a non-nil *noise.CipherState whose Cipher() yields a cipher.AEAD.
  2. Ensure the noise handshake completes and a cipher state is set before sending data-plane traffic.
  3. Add a nil check on the cipher state before calling EncryptDanger in the send path.

Example fix

// before
out, err := cs.EncryptDanger(out, ad, plaintext, n, nb) // cs may be nil
// after
if cs == nil {
    return errors.New("cipher state not initialized; handshake incomplete")
}
out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)
Defensive patterns

Strategy: type-guard

Validate before calling

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

Type guard

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

Try / catch

out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)
if err != nil && err.Error() == "no cipher state available to encrypt" {
    // trigger re-handshake
}

Prevention

When it happens

Trigger: Calling NewCipherStateAESGCM(nil) or constructing CipherStateAESGCM without a valid *noise.CipherState, then calling EncryptDanger; the test TestCipherStateNilSafety exercises exactly this.

Common situations: Handshake completed without establishing a cipher key (e.g. skipped or failed handshake), caching a nil cipher state and using it before rekey, or wiring a custom noise CipherState that returns nil.

Related errors


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