{"record":{"id":"bd7f393eb7c0f770","repo":"slackhq/nebula","slug":"no-cipher-state-available-to-encrypt-bd7f39","errorCode":null,"errorMessage":"no cipher state available to encrypt","messagePattern":"no cipher state available to encrypt","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"noiseutil/chachapoly.go","lineNumber":25,"sourceCode":"\n\t\"github.com/flynn/noise\"\n)\n\n// CipherStateChaChaPoly is the data-plane wrapper for the ChaCha20-Poly1305 AEAD cipher.\n// ChaCha20-Poly1305 uses little-endian nonce encoding per the Noise spec.\ntype CipherStateChaChaPoly struct {\n\tc cipher.AEAD\n}\n\n// NewCipherStateChaChaPoly extracts the underlying AEAD from the post-handshake noise.CipherState.\n// The caller is responsible for ensuring the noise cipher is actually ChaCha20-Poly1305.\nfunc NewCipherStateChaChaPoly(s *noise.CipherState) *CipherStateChaChaPoly {\n\treturn &CipherStateChaChaPoly{c: s.Cipher().(cipher.AEAD)}\n}\n\nfunc (s *CipherStateChaChaPoly) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) {\n\tif s == nil {\n\t\treturn nil, errors.New(\"no cipher state available to encrypt\")\n\t}\n\tif n >= RejectAfterMessages {\n\t\treturn nil, ErrMessageCounterExhausted\n\t}\n\tnb[0] = 0\n\tnb[1] = 0\n\tnb[2] = 0\n\tnb[3] = 0\n\tbinary.LittleEndian.PutUint64(nb[4:], n)\n\treturn s.c.Seal(out, nb, plaintext, ad), nil\n}\n\nfunc (s *CipherStateChaChaPoly) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) {\n\tif s == nil {\n\t\treturn []byte{}, nil\n\t}\n\tnb[0] = 0\n\tnb[1] = 0","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/noiseutil/chachapoly.go#L7-L43","documentation":"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.","triggerScenarios":"Calling EncryptDanger on a nil *CipherStateChaChaPoly, typically produced by NewCipherStateChaChaPoly(nil) or an unset post-handshake state; covered by TestCipherStateNilSafety.","commonSituations":"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.","solutions":["Verify the handshake produced a valid *noise.CipherState before wrapping with NewCipherStateChaChaPoly.","Guard the send path with a nil check on the ChaChaPoly cipher state.","If using CipherChoice_ChaChaPoly, confirm the peer negotiated the same cipher choice so a state is actually established."],"exampleFix":"// before\nif s == nil { return nil, errors.New(\"no cipher state available to encrypt\") }\n// after: caller-side\nif chacha == nil { rehandshake(); }","handlingStrategy":"type-guard","validationCode":"if cs == nil {\n    return errors.New(\"chachapoly cipher state not initialized\")\n}","typeGuard":"func hasCipherState(cs *noiseutil.CipherStateChaChaPoly) bool { return cs != nil }","tryCatchPattern":"out, err := cs.EncryptDanger(out, ad, plaintext, n, nb)\nif err != nil {\n    // nil state or counter exhaustion: force re-handshake\n}","preventionTips":["Gate the send path on a completed handshake flag.","Check for nil before caching the cipher state in session maps.","Confirm cipher choice negotiation matches the peer."],"tags":["noise","chachapoly","nil-state","encryption"],"backgroundTag":"uninitialized-cipher-state","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}