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

  1. Trim newlines/carriage returns from the input string before decoding (strings.TrimSpace / strings.ReplaceAll(s, "\r", "")).
  2. Read recipient files with per-line trimming so no CR survives.
  3. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/6b6e04c57d7085cb. Report an issue: GitHub.