AlexxIT/go2rtc · error

: unable to encrypt

Error message

%w: unable to encrypt

What it means

The ChaCha20-Poly1305 cipher suite stores its AEAD instance atomically and it is only initialized after the handshake computes keys. Calling Encrypt before that initialization completes means the suite is not ready, so the library returns errCipherSuiteNotInit wrapped with 'unable to encrypt' rather than using a nil cipher.

Solutions

  1. Wait for the DTLS handshake to complete (Connect/Handshake result) before writing application data.
  2. Serialize sends behind handshake completion (mutex or done-channel) to avoid racing key derivation.
  3. Verify the cipher suite was actually negotiated and initialized — check CustomCipherSuites() includes the ChaCha20 suite and the peer supports it.
  4. Re-create the session if the error persists; the suite instance may be permanently uninitialized.

Example fix

// before
conn.Write(data) // may race handshake
// after
select {
case <-conn.HandshakeDone():
	conn.Write(data)
case <-time.After(5 * time.Second):
	return errors.New("dtls handshake not completed")
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Gate sends on handshake completion
select {
case <-conn.HandshakeDone():
	// ok to encrypt now
case <-time.After(5 * time.Second):
	return errors.New("handshake incomplete; cannot encrypt")
}

Try / catch

out, err := suite.Encrypt(pkt, raw)
if err != nil {
	if errors.Is(err, errCipherSuiteNotInit) {
		<-handshakeDone // wait then retry once
		return suite.Encrypt(pkt, raw)
	}
	return err
}

Prevention

When it happens

Trigger: Calling Encrypt on TLSEcdhePskWithChacha20Poly1305Sha256 before the handshake populates c.aead — e.g. application data sent immediately after Dial before key derivation finished, or concurrent use racing the handshake.

Common situations: Application writes racing DTLS handshake completion; attempting to send data on a half-open session; missing/broken handshake trigger so aead is never set; misuse of the cipher suite directly instead of through the DTLS conn.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of AlexxIT/go2rtc@c245815e75 (2026-09-07). Data as JSON: /api/errors/3700c315673d178e. Report an issue: GitHub.

Appendix: source

Thrown at pkg/tutk/dtls/cipher.go:201

		)
	} else {
		aead, err = NewChaCha20Poly1305Cipher(
			keys.ServerWriteKey, keys.ServerWriteIV,
			keys.ClientWriteKey, keys.ClientWriteIV,
		)
	}
	if err != nil {
		return err
	}

	c.aead.Store(aead)
	return nil
}

func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
	aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
	if !ok {
		return nil, fmt.Errorf("%w: unable to encrypt", errCipherSuiteNotInit)
	}
	return aead.Encrypt(pkt, raw)
}

func (c *TLSEcdhePskWithChacha20Poly1305Sha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
	aead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)
	if !ok {
		return nil, fmt.Errorf("%w: unable to decrypt", errCipherSuiteNotInit)
	}
	return aead.Decrypt(h, raw)
}

func CustomCipherSuites() []dtls.CipherSuite {
	return []dtls.CipherSuite{
		NewTLSEcdhePskWithChacha20Poly1305Sha256(),
	}
}

View on GitHub (pinned to c245815e75)