Skip to content

Refactor JWT secret generating & decoding code #29172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
}

func runGenerateLfsJwtSecret(c *cli.Context) error {
_, jwtSecretBase64, err := generate.NewJwtSecretBase64()
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil {
return err
}
Expand Down
24 changes: 15 additions & 9 deletions modules/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package generate
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"time"

Expand Down Expand Up @@ -38,19 +39,24 @@ func NewInternalToken() (string, error) {
return internalToken, nil
}

// NewJwtSecret generates a new value intended to be used for JWT secrets.
func NewJwtSecret() ([]byte, error) {
bytes := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, bytes)
if err != nil {
const defaultJwtSecretLen = 32

// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
func DecodeJwtSecretBase64(src string) ([]byte, error) {
encoding := base64.RawURLEncoding
decoded := make([]byte, encoding.DecodedLen(len(src))+3)
if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
return nil, err
} else if n != defaultJwtSecretLen {
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
}
return bytes, nil
return decoded[:defaultJwtSecretLen], nil
}

// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
func NewJwtSecretBase64() ([]byte, string, error) {
bytes, err := NewJwtSecret()
// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
func NewJwtSecretWithBase64() ([]byte, string, error) {
bytes := make([]byte, defaultJwtSecretLen)
_, err := io.ReadFull(rand.Reader, bytes)
if err != nil {
return nil, "", err
}
Expand Down
34 changes: 34 additions & 0 deletions modules/generate/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package generate

import (
"encoding/base64"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestDecodeJwtSecretBase64(t *testing.T) {
_, err := DecodeJwtSecretBase64("abcd")
assert.ErrorContains(t, err, "invalid base64 decoded length")
_, err = DecodeJwtSecretBase64(strings.Repeat("a", 64))
assert.ErrorContains(t, err, "invalid base64 decoded length")

str32 := strings.Repeat("x", 32)
encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
decoded32, err := DecodeJwtSecretBase64(encoded32)
assert.NoError(t, err)
assert.Equal(t, str32, string(decoded32))
}

func TestNewJwtSecretWithBase64(t *testing.T) {
secret, encoded, err := NewJwtSecretWithBase64()
assert.NoError(t, err)
assert.Len(t, secret, 32)
decoded, err := DecodeJwtSecretBase64(encoded)
assert.NoError(t, err)
assert.Equal(t, secret, decoded)
}
6 changes: 2 additions & 4 deletions modules/setting/lfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
package setting

import (
"encoding/base64"
"fmt"
"time"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/util"
)

// LFS represents the configuration for Git LFS
Expand Down Expand Up @@ -62,9 +60,9 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
}

LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(LFS.JWTSecretBase64)
if err != nil {
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
}
Expand Down
7 changes: 2 additions & 5 deletions modules/setting/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
package setting

import (
"encoding/base64"
"math"
"path/filepath"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)

// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
Expand Down Expand Up @@ -137,13 +135,12 @@ func loadOAuth2From(rootCfg ConfigProvider) {
}

if InstallLock {
if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
key, err := generate.NewJwtSecret()
if _, err := generate.DecodeJwtSecretBase64(OAuth2.JWTSecretBase64); err != nil {
_, OAuth2.JWTSecretBase64, err = generate.NewJwtSecretWithBase64()
if err != nil {
log.Fatal("error generating JWT secret: %v", err)
}

OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
Expand Down
11 changes: 0 additions & 11 deletions modules/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package util
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"math/big"
"strconv"
Expand Down Expand Up @@ -246,13 +245,3 @@ func ToFloat64(number any) (float64, error) {
func ToPointer[T any](val T) *T {
return &val
}

func Base64FixedDecode(encoding *base64.Encoding, src []byte, length int) ([]byte, error) {
decoded := make([]byte, encoding.DecodedLen(len(src))+3)
if n, err := encoding.Decode(decoded, src); err != nil {
return nil, err
} else if n != length {
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
}
return decoded[:length], nil
}
14 changes: 0 additions & 14 deletions modules/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package util

import (
"encoding/base64"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -234,16 +233,3 @@ func TestToPointer(t *testing.T) {
val123 := 123
assert.False(t, &val123 == ToPointer(val123))
}

func TestBase64FixedDecode(t *testing.T) {
_, err := Base64FixedDecode(base64.RawURLEncoding, []byte("abcd"), 32)
assert.ErrorContains(t, err, "invalid base64 decoded length")
_, err = Base64FixedDecode(base64.RawURLEncoding, []byte(strings.Repeat("a", 64)), 32)
assert.ErrorContains(t, err, "invalid base64 decoded length")

str32 := strings.Repeat("x", 32)
encoded32 := base64.RawURLEncoding.EncodeToString([]byte(str32))
decoded32, err := Base64FixedDecode(base64.RawURLEncoding, []byte(encoded32), 32)
assert.NoError(t, err)
assert.Equal(t, str32, string(decoded32))
}
2 changes: 1 addition & 1 deletion routers/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
var lfsJwtSecret string
if _, lfsJwtSecret, err = generate.NewJwtSecretBase64(); err != nil {
if _, lfsJwtSecret, err = generate.NewJwtSecretWithBase64(); err != nil {
ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
return
}
Expand Down
3 changes: 2 additions & 1 deletion services/auth/source/oauth2/jwtsigningkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -336,7 +337,7 @@ func InitSigningKey() error {
// loadSymmetricKey checks if the configured secret is valid.
// If it is not valid, it will return an error.
func loadSymmetricKey() (any, error) {
return util.Base64FixedDecode(base64.RawURLEncoding, []byte(setting.OAuth2.JWTSecretBase64), 32)
return generate.DecodeJwtSecretBase64(setting.OAuth2.JWTSecretBase64)
}

// loadOrCreateAsymmetricKey checks if the configured private key exists.
Expand Down