{"record":{"id":"9e628bde2ea66a10","repo":"gotify/server","slug":"w-private-key-must-be-d-bytes","errorCode":null,"errorMessage":"%w: private key must be %d bytes","messagePattern":"%w: private key must be (.+?) bytes","errorType":"exception","errorClass":"errCannotParseToken","httpStatus":null,"severity":"error","filePath":"auth/token.go","lineNumber":128,"sourceCode":"\tif !found {\n\t\treturn nil, fmt.Errorf(\"%w: token must start with %s\", errCannotParseToken, enhancedTokenPrefix)\n\t}\n\n\t// count number of dots, one dot -> ident then private key, three dots -> ident, public key, challenge then signature\n\tfields := strings.SplitN(token, \".\", 4)\n\tif len(fields) != 2 && len(fields) != 4 {\n\t\treturn nil, fmt.Errorf(\"%w: token must have 2 or 4 fields separated by dots\", errCannotParseToken)\n\t}\n\tident := fields[0]\n\tpkOrPubkeyB64 := fields[1]\n\tpkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))\n\tpkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: base64 decode failed: %w\", errCannotParseToken, err)\n\t}\n\tif len(fields) == 2 {\n\t\tif pkOrPubkeyBytesLen != ed25519.SeedSize {\n\t\t\treturn nil, fmt.Errorf(\"%w: private key must be %d bytes\", errCannotParseToken, ed25519.SeedSize)\n\t\t}\n\t\treturn &EnhancedToken{\n\t\t\tident:        ident,\n\t\t\tpubOrPrivKey: pkOrPubkey,\n\t\t}, nil\n\t}\n\tif pkOrPubkeyBytesLen != ed25519.PublicKeySize {\n\t\treturn nil, fmt.Errorf(\"%w: public key must be %d bytes\", errCannotParseToken, ed25519.PublicKeySize)\n\t}\n\ttimestampStr := fields[2]\n\ttimestamp, err := strconv.ParseInt(timestampStr, 10, 64)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: timestamp must be an integer: %w\", errCannotParseToken, err)\n\t}\n\tsignatureB64 := fields[3]\n\tsignatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))\n\tif signatureBytesLen != ed25519.SignatureSize {\n\t\treturn nil, fmt.Errorf(\"%w: signature must be %d bytes\", errCannotParseToken, ed25519.SignatureSize)","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/auth/token.go#L110-L146","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: wrong-size key\nkey := ed25519.Sign(nil, seed) // 64-byte signature pasted as key\n// after: encode the 32-byte seed\nkey := base64.RawURLEncoding.EncodeToString(seed) // 32 bytes -> 43 chars","handlingStrategy":"validation","validationCode":"seg := strings.Split(strings.TrimPrefix(tok, \"gtfy\"), \".\")[1]\nif base64.RawURLEncoding.DecodedLen(len(seg)) != ed25519.SeedSize {\n    return errors.New(\"2-field token key must decode to 32 bytes\")\n}","typeGuard":"func isEd25519SeedSegment(seg string) bool {\n    return len(seg) == 43 && base64.RawURLEncoding.DecodedLen(len(seg)) == ed25519.SeedSize\n}","tryCatchPattern":"if _, err := auth.ParseEnhancedToken(raw); err != nil {\n    if strings.Contains(err.Error(), \"private key must be\") {\n        return fmt.Errorf(\"wrong key material for private-key token: %w\", err)\n    }\n}","preventionTips":["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."],"tags":["auth","token","ed25519","crypto"],"backgroundTag":"invalid-token-format","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}