{"record":{"id":"11f20b47f126e6c5","repo":"FiloSottile/age","slug":"unexpected-newline-character","errorCode":null,"errorMessage":"unexpected newline character","messagePattern":"unexpected newline character","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"armor/armor.go","lineNumber":165,"sourceCode":"\t\t}\n\t\tr.started = true\n\t}\n\tline, err := getLine()\n\tif err != nil {\n\t\treturn 0, r.setErr(err)\n\t}\n\tif string(line) == Footer {\n\t\treturn 0, r.setErr(drainTrailing())\n\t}\n\tif len(line) == 0 {\n\t\treturn 0, r.setErr(errors.New(\"empty line in armored data\"))\n\t}\n\tif len(line) > format.ColumnsPerLine {\n\t\treturn 0, r.setErr(errors.New(\"column limit exceeded\"))\n\t}\n\t// Reject newline characters ignored by base64.Decode.\n\tif bytes.ContainsAny(line, \"\\n\\r\") {\n\t\treturn 0, r.setErr(errors.New(\"unexpected newline character\"))\n\t}\n\tr.unread = r.buf[:]\n\tn, err := base64.StdEncoding.Strict().Decode(r.unread, line)\n\tif err != nil {\n\t\treturn 0, r.setErr(err)\n\t}\n\tr.unread = r.unread[:n]\n\n\tif n < format.BytesPerLine {\n\t\tline, err := getLine()\n\t\tif err != nil {\n\t\t\treturn 0, r.setErr(err)\n\t\t}\n\t\tif string(line) != Footer {\n\t\t\treturn 0, r.setErr(fmt.Errorf(\"invalid closing line: %q\", line))\n\t\t}\n\t\tr.setErr(drainTrailing())\n\t}","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/armor/armor.go#L147-L183","documentation":"The armored reader splits input into lines, but base64 decoding is strict, and newline characters inside a 'line' would be silently ignored by the decoder, enabling malleability. To prevent this, Read explicitly rejects any line containing \\n or \\r with this error.","triggerScenarios":"Reading an armored stream where a body line still contains embedded newline or carriage-return characters — typically CR bytes from Windows CRLF files surviving line splitting, or a custom Reader passed to armor.NewReader whose Read returns lines split unusually.","commonSituations":"Transferring .age armored files via protocols or scripts that rewrite line endings (CRLF vs LF), FTP ASCII-mode transfers on Windows, or wrappers around the reader that rejoin/split buffers incorrectly.","solutions":["Convert the file to Unix line endings (dos2unix or sed 's/\\r$//') and retry.","Transfer the file in binary mode / byte-safe channels instead of text-normalizing protocols.","If using a custom io.Reader feeding armor.NewReader, ensure it returns raw bytes without altering line endings."],"exampleFix":"// before: ASCII-mode transfer adds CRs\n// after\nsed 's/\\r$//' file.age > file-unix.age\n# or use ftp binary mode","handlingStrategy":"validation","validationCode":"// Normalize line endings before handing data to the armor reader\nfunc normalizeCRLF(r io.Reader) (io.Reader, error) {\n    data, err := io.ReadAll(r)\n    if err != nil {\n        return nil, err\n    }\n    return bytes.NewReader(bytes.ReplaceAll(data, []byte(\"\\r\\n\"), []byte(\"\\n\"))), nil\n}","typeGuard":"func hasEmbeddedNewlines(line []byte) bool {\n    return bytes.ContainsAny(line, \"\\n\\r\")\n}","tryCatchPattern":"_, err := io.ReadAll(armor.NewReader(f))\nif err != nil && err.Error() == \"unexpected newline character\" {\n    // re-read after normalizing CR/LF in the file\n}","preventionTips":["Transfer .age files in binary mode only.","Convert CRLF to LF (dos2unix) after text-based transfers.","Never post-process armored bytes with line-ending converting tools."],"tags":["go","age","armor","reader","newline","crlf"],"backgroundTag":"unexpected-newline","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}