gotify/server · error · errCannotParseToken

%w: base64 decode failed: %w

Error message

%w: base64 decode failed: %w

What it means

The key field of an enhanced token is base64 RawURL-encoded. If base64.RawURLEncoding.DecodeString fails on that segment, ParseEnhancedToken returns errCannotParseToken wrapped with "base64 decode failed" and the underlying decode error. It indicates the token's key segment was corrupted or never valid base64url.

Source

Thrown at auth/token.go:124

// ParseEnhancedToken parses a string into an EnhancedToken.
func ParseEnhancedToken(token string) (*EnhancedToken, error) {
	token, found := strings.CutPrefix(token, enhancedTokenPrefix)
	if !found {
		return nil, fmt.Errorf("%w: token must start with %s", errCannotParseToken, enhancedTokenPrefix)
	}

	// count number of dots, one dot -> ident then private key, three dots -> ident, public key, challenge then signature
	fields := strings.SplitN(token, ".", 4)
	if len(fields) != 2 && len(fields) != 4 {
		return nil, fmt.Errorf("%w: token must have 2 or 4 fields separated by dots", errCannotParseToken)
	}
	ident := fields[0]
	pkOrPubkeyB64 := fields[1]
	pkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))
	pkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)
	if err != nil {
		return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
	}
	if len(fields) == 2 {
		if pkOrPubkeyBytesLen != ed25519.SeedSize {
			return nil, fmt.Errorf("%w: private key must be %d bytes", errCannotParseToken, ed25519.SeedSize)
		}
		return &EnhancedToken{
			ident:        ident,
			pubOrPrivKey: pkOrPubkey,
		}, nil
	}
	if pkOrPubkeyBytesLen != ed25519.PublicKeySize {
		return nil, fmt.Errorf("%w: public key must be %d bytes", errCannotParseToken, ed25519.PublicKeySize)
	}
	timestampStr := fields[2]
	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("%w: timestamp must be an integer: %w", errCannotParseToken, err)
	}

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Re-copy the token verbatim from its source without transformation or line-wrapping.
  2. Ensure the key segment uses Raw URL-safe base64 (no '=' padding, '-'/'_' alphabet).
  3. Verify the stored token wasn't URL-encoded/HTML-escaped and decode if needed.
  4. Regenerate the token via the API if the original is unrecoverable.

Example fix

// before: padded standard base64
tok := "gtfyident.aGVsbG8="
// after: raw url-safe base64
tok := "gtfyident.aGVsbG8"
Defensive patterns

Strategy: validation

Validate before calling

seg := strings.Split(strings.TrimPrefix(tok, "gtfy"), ".")[1]
if _, err := base64.RawURLEncoding.DecodeString(seg); err != nil {
    return fmt.Errorf("token key segment is not raw url-safe base64: %w", err)
}

Type guard

func isRawURLBase64(s string) bool {
    _, err := base64.RawURLEncoding.DecodeString(s)
    return err == nil
}

Try / catch

if _, err := auth.ParseEnhancedToken(raw); err != nil {
    if strings.Contains(err.Error(), "base64 decode failed") {
        return fmt.Errorf("token corrupted in transit/storage: %w", err)
    }
}

Prevention

When it happens

Trigger: Parsing a token whose second dot-field contains characters outside the base64url alphabet (+, /, = padding, whitespace) or an impossible length — usually from copy/paste corruption, URL-encoding, or a truncated key.

Common situations: Tokens pasted through Markdown/HTML that re-encoded characters; shells mangling '_'/'-'; storing tokens in systems that wrap lines; manually re-encoding with standard base64 (with padding).

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/5146f11afadc3715. Report an issue: GitHub.