gotify/server · error · errCannotParseToken
%w: private key must be %d bytes
Error message
%w: private key must be %d bytes
What it means
For the 2-field (private-key) form of an enhanced token, the decoded key bytes must be exactly ed25519.SeedSize (32 bytes). A different decoded length returns errCannotParseToken wrapped with "private key must be 32 bytes", since ed25519 signing requires a 32-byte seed.
Source
Thrown at auth/token.go:128
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)
}
signatureB64 := fields[3]
signatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))
if signatureBytesLen != ed25519.SignatureSize {
return nil, fmt.Errorf("%w: signature must be %d bytes", errCannotParseToken, ed25519.SignatureSize)View on GitHub (pinned to 14bfc25627)
Solutions
- Use the 4-field public-key form if you hold a public key + challenge + signature instead.
- Ensure the private segment is the raw 32-byte ed25519 seed encoded as base64url (43 chars).
- Regenerate the keypair/token from the client library so encoding and sizes are correct.
- Check for truncation — the base64 segment must be exactly 43 characters for a 32-byte seed.
Example fix
// before: wrong-size key key := ed25519.Sign(nil, seed) // 64-byte signature pasted as key // after: encode the 32-byte seed key := base64.RawURLEncoding.EncodeToString(seed) // 32 bytes -> 43 chars
Defensive patterns
Strategy: validation
Validate before calling
seg := strings.Split(strings.TrimPrefix(tok, "gtfy"), ".")[1]
if base64.RawURLEncoding.DecodedLen(len(seg)) != ed25519.SeedSize {
return errors.New("2-field token key must decode to 32 bytes")
} Type guard
func isEd25519SeedSegment(seg string) bool {
return len(seg) == 43 && base64.RawURLEncoding.DecodedLen(len(seg)) == ed25519.SeedSize
} Try / catch
if _, err := auth.ParseEnhancedToken(raw); err != nil {
if strings.Contains(err.Error(), "private key must be") {
return fmt.Errorf("wrong key material for private-key token: %w", err)
}
} Prevention
- Only place the 32-byte ed25519 seed in the 2-field private-key form.
- Expect the base64 segment to be exactly 43 characters; check for truncation.
- Generate tokens with the official client rather than manual concatenation.
When it happens
Trigger: Parsing a "gtfyident.<key>" token whose base64 field decodes to something other than 32 bytes — e.g. a public key (also 32 bytes is fine, but a signature or wrong material isn't), a raw password, or a hex-encoded key pasted as if base64.
Common situations: Manually constructing tokens with the wrong key material; pasting a public key where a private seed is required with the 2-field form; truncated keys after copy/paste.
Related errors
- %w: public key must be %d bytes
- invalid token
- %w: token must start with %s
- %w: token must have 2 or 4 fields separated by dots
- %w: base64 decode failed: %w
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/9e628bde2ea66a10.
Report an issue: GitHub.