{"record":{"id":"4f91a8f918db1b29","repo":"FiloSottile/age","slug":"trailing-data-after-end-of-encrypted-file","errorCode":null,"errorMessage":"trailing data after end of encrypted file","messagePattern":"trailing data after end of encrypted file","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/stream/stream.go","lineNumber":103,"sourceCode":"\t\treturn 0, nil\n\t}\n\n\tlast, err := r.readChunk()\n\tif err != nil {\n\t\tr.err = err\n\t\treturn 0, err\n\t}\n\n\tn := copy(p, r.unread)\n\tr.unread = r.unread[n:]\n\n\tif last {\n\t\t// Ensure there is an EOF after the last chunk as expected. In other\n\t\t// words, check for trailing data after a full-length final chunk.\n\t\t// Hopefully, the underlying reader supports returning EOF even if it\n\t\t// had previously returned an EOF to ReadFull.\n\t\tif _, err := io.ReadFull(r.src, make([]byte, 1)); err == nil {\n\t\t\tr.err = errors.New(\"trailing data after end of encrypted file\")\n\t\t} else if err != io.EOF {\n\t\t\tr.err = fmt.Errorf(\"non-EOF error reading after end of encrypted file: %w\", err)\n\t\t} else {\n\t\t\tr.err = io.EOF\n\t\t}\n\t}\n\n\treturn n, nil\n}\n\n// readChunk reads the next chunk of ciphertext from r.src and makes it available\n// in r.unread. last is true if the chunk was marked as the end of the message.\n// readChunk must not be called again after returning a last chunk or an error.\nfunc (r *DecryptReader) readChunk() (last bool, err error) {\n\tif len(r.unread) != 0 {\n\t\tpanic(\"stream: internal error: readChunk called with dirty buffer\")\n\t}\n","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/internal/stream/stream.go#L85-L121","documentation":"After the stream reader consumed the final (last-chunk-flagged) chunk, it expects an immediate EOF from the underlying source. Any additional byte after the last chunk means the encrypted file has extra trailing data, so the reader reports this error instead of silently ending. Trailing bytes may indicate concatenation of files, corruption, or tampering.","triggerScenarios":"Reading an age stream (stream.NewReader) where the ciphertext source contains one or more bytes after the terminal full-length final chunk, detected via io.ReadFull(src, 1) returning no error.","commonSituations":"Appending to an already-complete encrypted file; concatenating two age files; a wrapper storing extra padding/records after the payload; truncated re-uploads that added junk bytes.","solutions":["Verify the ciphertext is not being written after encryption completes (close the writer before sharing the file).","Re-encrypt from the original plaintext to produce a clean file.","Check for accidental file concatenation or appending logic (e.g. os.O_APPEND on an existing file) and open with O_TRUNC instead."],"exampleFix":"// before\nf, _ := os.OpenFile(\"file.age\", os.O_APPEND|os.O_WRONLY, 0) // appends to existing ciphertext\n// after\nf, _ := os.OpenFile(\"file.age\", os.O_TRUNC|os.O_WRONLY, 0)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"n, err := io.Copy(out, r)\nif err != nil && strings.Contains(err.Error(), \"trailing data after end of encrypted file\") {\n    // ciphertext has extra bytes: treat as corrupted, do not trust partial output\n    return fmt.Errorf(\"ciphertext malformed: %w\", err)\n}","preventionTips":["Close the stream.Writer and confirm no error before using/sharing the ciphertext.","Never append to an existing .age file; always write to a fresh file.","Verify checksums of ciphertext after transport."],"tags":["streaming","ciphertext","trailing-data"],"backgroundTag":"trailing-data-after-encrypted-file","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}