Skip to content

Commit 2fa9a34

Browse files
committed
fix
1 parent e94723f commit 2fa9a34

File tree

10 files changed

+158
-64
lines changed

10 files changed

+158
-64
lines changed

cmd/generate.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
microcmdGenerateInternalToken,
3232
microcmdGenerateLfsJwtSecret,
3333
microcmdGenerateSecretKey,
34+
microcmdGenerateGeneralWebSecret,
3435
},
3536
}
3637

@@ -52,6 +53,12 @@ var (
5253
Usage: "Generate a new SECRET_KEY",
5354
Action: runGenerateSecretKey,
5455
}
56+
57+
microcmdGenerateGeneralWebSecret = &cli.Command{
58+
Name: "GENERAL_WEB_SECRET",
59+
Usage: "Generate a new GENERAL_WEB_SECRET",
60+
Action: runGenerateGeneralWebSecret,
61+
}
5562
)
5663

5764
func runGenerateInternalToken(c *cli.Context) error {
@@ -84,6 +91,18 @@ func runGenerateLfsJwtSecret(c *cli.Context) error {
8491
return nil
8592
}
8693

94+
func runGenerateGeneralWebSecret(c *cli.Context) error {
95+
_, webSecretBase64, err := generate.NewGeneralWebSecretWithBase64()
96+
if err != nil {
97+
return err
98+
}
99+
fmt.Printf("%s", webSecretBase64)
100+
if isatty.IsTerminal(os.Stdout.Fd()) {
101+
fmt.Printf("\n")
102+
}
103+
return nil
104+
}
105+
87106
func runGenerateSecretKey(c *cli.Context) error {
88107
secretKey, err := generate.NewSecretKey()
89108
if err != nil {

custom/conf/app.example.ini

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,18 +428,24 @@ INSTALL_LOCK = false
428428
;;
429429
;; Global secret key that will be used
430430
;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
431-
SECRET_KEY =
431+
;SECRET_KEY =
432432
;;
433433
;; Alternative location to specify secret key, instead of this file; you cannot specify both this and SECRET_KEY, and must pick one
434434
;; This key is VERY IMPORTANT. If you lose it, the data encrypted by it (like 2FA secret) can't be decrypted anymore.
435435
;SECRET_KEY_URI = file:/etc/gitea/secret_key
436436
;;
437437
;; Secret used to validate communication within Gitea binary.
438-
INTERNAL_TOKEN =
438+
;INTERNAL_TOKEN =
439439
;;
440440
;; Alternative location to specify internal token, instead of this file; you cannot specify both this and INTERNAL_TOKEN, and must pick one
441441
;INTERNAL_TOKEN_URI = file:/etc/gitea/internal_token
442442
;;
443+
;; A general secret used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc)
444+
;GENERAL_WEB_SECRET =
445+
;;
446+
;; Alternative location to specify general web secret (eg: file:/etc/gitea/general_web_secret), you cannot specify both this and GENERAL_WEB_SECRET, and must pick one
447+
;GENERAL_WEB_SECRET_URI =
448+
;;
443449
;; How long to remember that a user is logged in before requiring relogin (in days)
444450
;LOGIN_REMEMBER_DAYS = 31
445451
;;

docs/content/administration/config-cheat-sheet.en-us.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,8 @@ And the following unique queues:
556556
- `IMPORT_LOCAL_PATHS`: **false**: Set to `false` to prevent all users (including admin) from importing local path on server.
557557
- `INTERNAL_TOKEN`: **\<random at every install if no uri set\>**: Secret used to validate communication within Gitea binary.
558558
- `INTERNAL_TOKEN_URI`: **_empty_**: Instead of defining INTERNAL_TOKEN in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`)
559+
- `GENERAL_WEB_SECRET`: **\<random at every install if no uri set\>**: A general secret used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc).
560+
- `GENERAL_WEB_SECRET_URI`: **_empty_**: Instead of defining GENERAL_WEB_SECRET in the configuration, this configuration option can be used to give Gitea a path to a file that contains the general web secret (example value: `file:/etc/gitea/genearl_web_secret`)
559561
- `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[argon2, pbkdf2, pbkdf2_v1, pbkdf2_hi, scrypt, bcrypt\], argon2 and scrypt will spend significant amounts of memory.
560562
- Note: The default parameters for `pbkdf2` hashing have changed - the previous settings are available as `pbkdf2_v1` but are not recommended.
561563
- The hash functions may be tuned by using `$` after the algorithm:

modules/generate/generate.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,37 @@ func NewInternalToken() (string, error) {
3939
return internalToken, nil
4040
}
4141

42-
const defaultJwtSecretLen = 32
43-
44-
// DecodeJwtSecretBase64 decodes a base64 encoded jwt secret into bytes, and check its length
4542
func DecodeJwtSecretBase64(src string) ([]byte, error) {
43+
return decodeGeneralSecretBase64(src, 32)
44+
}
45+
46+
func NewJwtSecretWithBase64() ([]byte, string, error) {
47+
return newGeneralSecretWithBase64(32)
48+
}
49+
50+
func DecodeGeneralWebSecretBase64(src string) ([]byte, error) {
51+
return decodeGeneralSecretBase64(src, 32)
52+
}
53+
54+
func NewGeneralWebSecretWithBase64() ([]byte, string, error) {
55+
return newGeneralSecretWithBase64(32)
56+
}
57+
58+
// decodeGeneralSecretBase64 decodes a base64 encoded secret into bytes, and check its length
59+
func decodeGeneralSecretBase64(src string, length int) ([]byte, error) {
4660
encoding := base64.RawURLEncoding
4761
decoded := make([]byte, encoding.DecodedLen(len(src))+3)
4862
if n, err := encoding.Decode(decoded, []byte(src)); err != nil {
4963
return nil, err
50-
} else if n != defaultJwtSecretLen {
51-
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen)
64+
} else if n != length {
65+
return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, length)
5266
}
53-
return decoded[:defaultJwtSecretLen], nil
67+
return decoded[:length], nil
5468
}
5569

56-
// NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file
57-
func NewJwtSecretWithBase64() ([]byte, string, error) {
58-
bytes := make([]byte, defaultJwtSecretLen)
70+
// newGeneralSecretWithBase64 generates a secret with its base64 encoded value intended to be used for saving into config file
71+
func newGeneralSecretWithBase64(length int) ([]byte, string, error) {
72+
bytes := make([]byte, length)
5973
_, err := io.ReadFull(rand.Reader, bytes)
6074
if err != nil {
6175
return nil, "", err

modules/setting/oauth2.go

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package setting
66
import (
77
"math"
88
"path/filepath"
9-
"sync/atomic"
109

1110
"code.gitea.io/gitea/modules/generate"
1211
"code.gitea.io/gitea/modules/log"
@@ -137,9 +136,9 @@ func loadOAuth2From(rootCfg ConfigProvider) {
137136
}
138137

139138
if InstallLock {
140-
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
139+
_, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
141140
if err != nil {
142-
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
141+
_, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
143142
if err != nil {
144143
log.Fatal("error generating JWT secret: %v", err)
145144
}
@@ -153,27 +152,5 @@ func loadOAuth2From(rootCfg ConfigProvider) {
153152
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
154153
}
155154
}
156-
generalSigningSecret.Store(&jwtSecretBytes)
157155
}
158156
}
159-
160-
// generalSigningSecret is used as container for a []byte value
161-
// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
162-
var generalSigningSecret atomic.Pointer[[]byte]
163-
164-
func GetGeneralTokenSigningSecret() []byte {
165-
old := generalSigningSecret.Load()
166-
if old == nil || len(*old) == 0 {
167-
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
168-
if err != nil {
169-
log.Fatal("Unable to generate general JWT secret: %s", err.Error())
170-
}
171-
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
172-
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
173-
LogStartupProblem(1, log.WARN, "OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
174-
return jwtSecret
175-
}
176-
return *generalSigningSecret.Load()
177-
}
178-
return *old
179-
}

modules/setting/oauth2_test.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,9 @@ package setting
66
import (
77
"testing"
88

9-
"code.gitea.io/gitea/modules/generate"
10-
"code.gitea.io/gitea/modules/test"
11-
129
"github.com/stretchr/testify/assert"
1310
)
1411

15-
func TestGetGeneralSigningSecret(t *testing.T) {
16-
// when there is no general signing secret, it should be generated, and keep the same value
17-
assert.Nil(t, generalSigningSecret.Load())
18-
s1 := GetGeneralTokenSigningSecret()
19-
assert.NotNil(t, s1)
20-
s2 := GetGeneralTokenSigningSecret()
21-
assert.Equal(t, s1, s2)
22-
23-
// the config value should always override any pre-generated value
24-
cfg, _ := NewConfigProviderFromData(`
25-
[oauth2]
26-
JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
27-
`)
28-
defer test.MockVariableValue(&InstallLock, true)()
29-
loadOAuth2From(cfg)
30-
actual := GetGeneralTokenSigningSecret()
31-
expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
32-
assert.Len(t, actual, 32)
33-
assert.EqualValues(t, expected, actual)
34-
}
35-
3612
func TestOauth2DefaultApplications(t *testing.T) {
3713
cfg, _ := NewConfigProviderFromData(``)
3814
loadOAuth2From(cfg)

modules/setting/secret_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package setting
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"code.gitea.io/gitea/modules/log"
8+
"code.gitea.io/gitea/modules/test"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestGeneralWebSecret(t *testing.T) {
14+
osExiter := test.MockedOsExiter{}
15+
defer test.MockVariableValue(&log.OsExiter, osExiter.Exit)()
16+
assert.Nil(t, GeneralWebSecretBytes)
17+
_ = GetGeneralTokenSigningSecret()
18+
assert.Equal(t, 1, osExiter.FetchCode())
19+
20+
tmpFile := t.TempDir() + "/app.ini"
21+
_ = os.WriteFile(tmpFile, []byte("[security]\nINSTALL_LOCK=true"), 0o644)
22+
cfg, _ := NewConfigProviderFromFile(tmpFile)
23+
loadSecurityFrom(cfg)
24+
generated := GeneralWebSecretBytes
25+
assert.Len(t, generated, 32)
26+
27+
cfg, _ = NewConfigProviderFromFile(tmpFile)
28+
GeneralWebSecretBytes = nil
29+
loadSecurityFrom(cfg)
30+
loaded := GeneralWebSecretBytes
31+
assert.Equal(t, generated, loaded)
32+
33+
_ = os.WriteFile(tmpFile, []byte("[security]\nINSTALL_LOCK=true"), 0o644)
34+
cfg, _ = NewConfigProviderFromFile(tmpFile)
35+
loadSecurityFrom(cfg)
36+
generated2 := GeneralWebSecretBytes
37+
assert.Len(t, generated2, 32)
38+
assert.NotEqual(t, generated, generated2)
39+
}

modules/setting/security.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ var (
1717
// Security settings
1818
InstallLock bool
1919
SecretKey string
20-
InternalToken string // internal access token
20+
InternalToken string // internal access token is used by Gitea cli to communicate with Gitea server
21+
GeneralWebSecretBytes []byte // general web secret is used for signing or encrypting web related contents (CSRF token, JWT token, validation, etc)
2122
LogInRememberDays int
2223
CookieRememberName string
2324
ReverseProxyAuthUser string
@@ -85,18 +86,37 @@ func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string {
8586
func generateSaveInternalToken(rootCfg ConfigProvider) {
8687
token, err := generate.NewInternalToken()
8788
if err != nil {
88-
log.Fatal("Error generate internal token: %v", err)
89+
log.Fatal("Failed to generate internal token: %v", err)
8990
}
9091

9192
InternalToken = token
9293
saveCfg, err := rootCfg.PrepareSaving()
9394
if err != nil {
94-
log.Fatal("Error saving internal token: %v", err)
95+
log.Fatal("Failed to save internal token: %v", err)
9596
}
9697
rootCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
9798
saveCfg.Section("security").Key("INTERNAL_TOKEN").SetValue(token)
9899
if err = saveCfg.Save(); err != nil {
99-
log.Fatal("Error saving internal token: %v", err)
100+
log.Fatal("Failed to save internal token: %v", err)
101+
}
102+
}
103+
104+
// generateSaveInternalToken generates and saves the internal token to app.ini
105+
func generateSaveGeneralWebSecret(rootCfg ConfigProvider) {
106+
secretBytes, secretString, err := generate.NewGeneralWebSecretWithBase64()
107+
if err != nil {
108+
log.Fatal("Failed to generate general web secret: %v", err)
109+
}
110+
111+
GeneralWebSecretBytes = secretBytes
112+
saveCfg, err := rootCfg.PrepareSaving()
113+
if err != nil {
114+
log.Fatal("Failed to save general web secret: %v", err)
115+
}
116+
rootCfg.Section("security").Key("GENERAL_WEB_SECRET").SetValue(secretString)
117+
saveCfg.Section("security").Key("GENERAL_WEB_SECRET").SetValue(secretString)
118+
if err = saveCfg.Save(); err != nil {
119+
log.Fatal("Failed to save general web secret: %v", err)
100120
}
101121
}
102122

@@ -147,6 +167,13 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
147167
generateSaveInternalToken(rootCfg)
148168
}
149169

170+
var err error
171+
generalWebSecret := loadSecret(sec, "GENERAL_WEB_SECRET_URI", "GENERAL_WEB_SECRET")
172+
GeneralWebSecretBytes, err = generate.DecodeGeneralWebSecretBase64(generalWebSecret)
173+
if InstallLock && err != nil {
174+
generateSaveGeneralWebSecret(rootCfg)
175+
}
176+
150177
cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",")
151178
if len(cfgdata) == 0 {
152179
cfgdata = []string{"off"}
@@ -169,3 +196,10 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
169196
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
170197
}
171198
}
199+
200+
func GetGeneralTokenSigningSecret() []byte {
201+
if len(GeneralWebSecretBytes) == 0 {
202+
log.Fatal("GeneralWebSecretBytes is empty")
203+
}
204+
return GeneralWebSecretBytes
205+
}

modules/test/utils.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"code.gitea.io/gitea/modules/json"
12+
"code.gitea.io/gitea/modules/util"
1213
)
1314

1415
// RedirectURL returns the redirect URL of a http response.
@@ -39,3 +40,20 @@ func MockVariableValue[T any](p *T, v T) (reset func()) {
3940
*p = v
4041
return func() { *p = old }
4142
}
43+
44+
type MockedOsExiter struct {
45+
exitCode int
46+
called bool
47+
}
48+
49+
func (m *MockedOsExiter) Exit(code int) {
50+
m.called = true
51+
m.exitCode = code
52+
}
53+
54+
func (m *MockedOsExiter) FetchCode() int {
55+
c := util.Iif(m.called, m.exitCode, -1)
56+
m.exitCode = 0
57+
m.called = false
58+
return c
59+
}

routers/install/install.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,15 @@ func SubmitInstall(ctx *context.Context) {
481481
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
482482
}
483483

484+
if len(setting.GeneralWebSecretBytes) == 0 {
485+
_, generalWebSecret, err := generate.NewGeneralWebSecretWithBase64()
486+
if err != nil {
487+
ctx.RenderWithErr(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
488+
return
489+
}
490+
cfg.Section("security").Key("GENERAL_WEB_SECRET").SetValue(generalWebSecret)
491+
}
492+
484493
// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
485494
if setting.SecretKey == "" {
486495
var secretKey string

0 commit comments

Comments
 (0)