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

  1. Check the captured raw packet length — the peer sent a record smaller than header+16 bytes; look for truncation or corruption upstream.
  2. Verify both peers negotiate the same DTLS record format/cipher suite version.
  3. Add packet-loss/duplication handling on the UDP transport and retransmit failed records.
  4. 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

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)