AlexxIT/go2rtc · error

: unable to decrypt

Error message

%w: unable to decrypt

What it means

Mirror of the Encrypt path: Decrypt on TLSEcdhePskWithChacha20Poly1305Sha256 requires the AEAD cipher to be initialized by the handshake. If the atomic pointer does not hold a *ChaCha20Poly1305Cipher (typically nil before key derivation), the library returns errCipherSuiteNotInit wrapped with 'unable to decrypt'.

Solutions

  1. Ensure the DTLS handshake fully completes before processing records; gate reads on handshake completion.
  2. Confirm the ChaCha20-Poly1305 suite is negotiated on both ends so aead gets populated.
  3. Protect against concurrent access — initialize/read c.aead through the DTLS layer's synchronization.
  4. Re-establish the session if aead remains uninitialized; the suite instance is unusable.

Example fix

// before
out, err := suite.Decrypt(h, raw)
// after
if !suite.Initialized() { // or <-handshakeDone
	return errors.New("cipher suite not ready; handshake incomplete")
}
out, err := suite.Decrypt(h, raw)
Defensive patterns

Strategy: try-catch

Validate before calling

// Only process records after key derivation
if !handshakeCompleted.Load() {
	return errors.New("records cannot be decrypted before handshake")
}

Type guard

func suiteReady(s *TLSEcdhePskWithChacha20Poly1305Sha256) bool {
	_, ok := s.aead.Load().(*ChaCha20Poly1305Cipher)
	return ok
}

Try / catch

out, err := suite.Decrypt(h, raw)
if err != nil {
	if errors.Is(err, errCipherSuiteNotInit) {
		return errors.New("handshake incomplete — wait for handshake done signal")
	}
	return err
}

Prevention

When it happens

Trigger: Calling Decrypt (directly or via record-layer processing) before the handshake has set c.aead — reading records on a session whose key material was never derived, or concurrent decrypts during handshake.

Common situations: Receiving encrypted flight data before handshake completes; using the cipher suite object standalone without running handshake; handshake aborted partway leaving aead unset; negotiated suite mismatch so the concrete type never loads.

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/01079bbc1339ac51. Report an issue: GitHub.

Appendix: source

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

		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)