FiloSottile/age · error
unexpected newline character
Error message
unexpected newline character
What it means
format.DecodeString decodes the strict base64 used in age headers and stanzas. Go's base64 decoder ignores CR and LF inside input, which would allow header malleability (the same logical key encoded differently), so DecodeString deliberately rejects any string containing \n or \r before decoding.
Source
Thrown at internal/format/format.go:36
type Header struct {
Recipients []*Stanza
MAC []byte
}
// Stanza is assignable to age.Stanza, and if this package is made public,
// age.Stanza can be made a type alias of this type.
type Stanza struct {
Type string
Args []string
Body []byte
}
var b64 = base64.RawStdEncoding.Strict()
func DecodeString(s string) ([]byte, error) {
// CR and LF are ignored by DecodeString, but we don't want any malleability.
if strings.ContainsAny(s, "\n\r") {
return nil, errors.New(`unexpected newline character`)
}
return b64.DecodeString(s)
}
var EncodeToString = b64.EncodeToString
const ColumnsPerLine = 64
const BytesPerLine = ColumnsPerLine / 4 * 3
// NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst.
func NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder {
w := &WrappedBase64Encoder{dst: dst}
w.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped))
return w
}
type WriterFunc func(p []byte) (int, error)View on GitHub (pinned to b74dce4cdb)
Solutions
- Trim newlines/carriage returns from the input string before decoding (strings.TrimSpace / strings.ReplaceAll(s, "\r", "")).
- Read recipient files with per-line trimming so no CR survives.
- Normalize the source data (dos2unix) before feeding it to any age parsing API.
Example fix
// before key, err := format.DecodeString(recipientLine) // recipientLine has trailing \r // after key, err := format.DecodeString(strings.TrimSpace(recipientLine))
Defensive patterns
Strategy: validation
Validate before calling
// Validate and sanitize before calling format.DecodeString
func safeDecode(s string) ([]byte, error) {
if strings.ContainsAny(s, "\n\r") {
return nil, errors.New("newline in base64 field")
}
return format.DecodeString(strings.TrimSpace(s))
} Type guard
func isSingleLineB64(s string) bool {
return !strings.ContainsAny(s, "\n\r")
} Try / catch
key, err := format.DecodeString(s)
if err != nil && strings.Contains(err.Error(), "unexpected newline character") {
key, err = format.DecodeString(strings.ReplaceAll(s, "\r", ""))
} Prevention
- Trim each line with strings.TrimSpace when reading recipient files.
- Normalize CRLF files (dos2unix) before parsing age headers/recipients.
- Never embed raw newlines when constructing stanza arguments programmatically.
When it happens
Trigger: Calling format.DecodeString (directly, or indirectly via age.ParseRecipients, ReadStanza, header parsing, or agessh.unwrap) with a string containing CR/LF — e.g. recipient lines read from a CRLF file, stanza arguments split incorrectly, or user-supplied recipient strings pasted with line breaks.
Common situations: Passing recipients from a Windows-edited recipients.txt (CRLF) into age without trimming, building stanzas by hand and accidentally joining fields with newlines, or parsing an age header where a base64 field spans a line boundary due to corruption.
Related errors
- unexpected newline character
- ArmoredWriter already closed
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/6b6e04c57d7085cb.
Report an issue: GitHub.