AlexxIT/go2rtc · error
ciphertext too short
Error message
ciphertext too short: %d <= %d
What it means
The DTLS ChaCha20-Poly1305 decrypt path validates that the record is longer than the record header plus the 16-byte AEAD tag before attempting to open the ciphertext. If the payload is too short to possibly contain valid ciphertext plus tag, decryption is skipped and this error is returned instead of a cryptic AEAD failure.
Solutions
- Check the captured raw packet length — the peer sent a record smaller than header+16 bytes; look for truncation or corruption upstream.
- Verify both peers negotiate the same DTLS record format/cipher suite version.
- Add packet-loss/duplication handling on the UDP transport and retransmit failed records.
- If it happens on handshake boundary, ensure the ChangeCipherSpec record is delivered before encrypted application data.
Example fix
// before
out, err := cipher.Decrypt(h, raw)
if err != nil { return err }
// after
if len(raw) <= recordHeaderSize+16 {
log.Printf("dropping short DTLS record: %d bytes", len(raw))
return nil // skip malformed record
}
out, err := cipher.Decrypt(h, raw) Defensive patterns
Strategy: validation
Validate before calling
// Validate record length before decrypting
if len(raw) <= recordHeaderSize+16 {
log.Printf("skipping short DTLS record: %d bytes", len(raw))
return nil
} Type guard
func isDecryptableRecord(raw []byte, hdrSize, tagLen int) bool {
return len(raw) > hdrSize+tagLen
} Try / catch
out, err := cipher.Decrypt(h, raw)
if err != nil {
if strings.Contains(err.Error(), "ciphertext too short") {
metrics.ShortRecordDropped.Inc()
return nil // drop malformed record
}
return err
} Prevention
- Check UDP transport for truncation/reassembly bugs
- Verify both peers use the same DTLS version and record framing
- Drop-and-count malformed records instead of failing the session
- Monitor packet corruption rates on lossy links
When it happens
Trigger: Calling Decrypt on a record whose total length is <= header size + chachaTagLength — i.e. truncated packet, a zero-length payload record, or corrupted framing on the wire. ChangeCipherSpec records bypass the check.
Common situations: Corrupted or truncated UDP datagrams on lossy networks (TUTK runs DTLS over UDP); packets reassembled incorrectly; a peer sending malformed DTLS records due to a version/implementation mismatch.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
AI-assisted analysis of AlexxIT/go2rtc@c245815e75 (2026-09-07).
Data as JSON: /api/errors/46e5a2cdac809a37.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/tutk/dtls/cipher.go:107
r := make([]byte, len(raw)+len(encryptedPayload))
copy(r, raw)
copy(r[len(raw):], encryptedPayload)
binary.BigEndian.PutUint16(r[pkt.Header.Size()-2:], uint16(len(r)-pkt.Header.Size()))
return r, nil
}
func (c *ChaCha20Poly1305Cipher) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
err := header.Unmarshal(in)
switch {
case err != nil:
return nil, err
case header.ContentType == protocol.ContentTypeChangeCipherSpec:
return in, nil
case len(in) <= header.Size()+chachaTagLength:
return nil, fmt.Errorf("ciphertext too short: %d <= %d", len(in), header.Size()+chachaTagLength)
}
nonce := computeNonce(c.remoteWriteIV, header.Epoch, header.SequenceNumber)
out := in[header.Size():]
additionalData := generateAEADAdditionalData(&header, len(out)-chachaTagLength)
out, err = c.remoteCipher.Open(out[:0], nonce, out, additionalData)
if err != nil {
return nil, fmt.Errorf("%w: %v", errDecryptPacket, err)
}
return append(in[:header.Size()], out...), nil
}
type TLSEcdhePskWithChacha20Poly1305Sha256 struct {
aead atomic.Value
}
View on GitHub (pinned to c245815e75)