mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-22 17:18:33 +00:00
[GITEA] rework long-term authentication
- The current architecture is inherently insecure, because you can construct the 'secret' cookie value with values that are available in the database. Thus provides zero protection when a database is dumped/leaked. - This patch implements a new architecture that's inspired from: [Paragonie Initiative](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#secure-remember-me-cookies). - Integration testing is added to ensure the new mechanism works. - Removes a setting, because it's not used anymore. (cherry picked from commiteff097448b
) [GITEA] rework long-term authentication (squash) add migration Reminder: the migration is run via integration tests as explained in the commit "[DB] run all Forgejo migrations in integration tests" (cherry picked from commit4accf7443c
) (cherry picked from commit 99d06e344ebc3b50bafb2ac4473dd95f057d1ddc) (cherry picked from commitd8bc98a8f0
) (cherry picked from commit6404845df9
) (cherry picked from commit72bdd4f3b9
) (cherry picked from commit4b01bb0ce8
)
This commit is contained in:
parent
d1e5d9d664
commit
c26ac31816
17 changed files with 365 additions and 154 deletions
|
@ -4,16 +4,14 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
const CookieNameFlash = "gitea_flash"
|
||||
|
@ -46,41 +44,13 @@ func (ctx *Context) GetSiteCookie(name string) string {
|
|||
return middleware.GetSiteCookie(ctx.Req, name)
|
||||
}
|
||||
|
||||
// GetSuperSecureCookie returns given cookie value from request header with secret string.
|
||||
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
|
||||
val := ctx.GetSiteCookie(name)
|
||||
return ctx.CookieDecrypt(secret, val)
|
||||
}
|
||||
|
||||
// CookieDecrypt returns given value from with secret string.
|
||||
func (ctx *Context) CookieDecrypt(secret, val string) (string, bool) {
|
||||
if val == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
text, err := hex.DecodeString(val)
|
||||
// SetLTACookie will generate a LTA token and add it as an cookie.
|
||||
func (ctx *Context) SetLTACookie(u *user_model.User) error {
|
||||
days := 86400 * setting.LogInRememberDays
|
||||
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)))
|
||||
if err != nil {
|
||||
return "", false
|
||||
return err
|
||||
}
|
||||
|
||||
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
||||
text, err = util.AESGCMDecrypt(key, text)
|
||||
return string(text), err == nil
|
||||
}
|
||||
|
||||
// SetSuperSecureCookie sets given cookie value to response header with secret string.
|
||||
func (ctx *Context) SetSuperSecureCookie(secret, name, value string, maxAge int) {
|
||||
text := ctx.CookieEncrypt(secret, value)
|
||||
ctx.SetSiteCookie(name, text, maxAge)
|
||||
}
|
||||
|
||||
// CookieEncrypt encrypts a given value using the provided secret
|
||||
func (ctx *Context) CookieEncrypt(secret, value string) string {
|
||||
key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
|
||||
text, err := util.AESGCMEncrypt(key, []byte(value))
|
||||
if err != nil {
|
||||
panic("error encrypting cookie: " + err.Error())
|
||||
}
|
||||
|
||||
return hex.EncodeToString(text)
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, lookup+":"+validator, days)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ var (
|
|||
SecretKey string
|
||||
InternalToken string // internal access token
|
||||
LogInRememberDays int
|
||||
CookieUserName string
|
||||
CookieRememberName string
|
||||
ReverseProxyAuthUser string
|
||||
ReverseProxyAuthEmail string
|
||||
|
@ -104,7 +103,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
|||
sec := rootCfg.Section("security")
|
||||
InstallLock = HasInstallLock(rootCfg)
|
||||
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(7)
|
||||
CookieUserName = sec.Key("COOKIE_USERNAME").MustString("gitea_awesome")
|
||||
SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY")
|
||||
if SecretKey == "" {
|
||||
// FIXME: https://github.com/go-gitea/gitea/issues/16832
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
@ -40,52 +36,3 @@ func CopyFile(src, dest string) error {
|
|||
}
|
||||
return os.Chmod(dest, si.Mode())
|
||||
}
|
||||
|
||||
// AESGCMEncrypt (from legacy package): encrypts plaintext with the given key using AES in GCM mode. should be replaced.
|
||||
func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||
return append(nonce, ciphertext...), nil
|
||||
}
|
||||
|
||||
// AESGCMDecrypt (from legacy package): decrypts ciphertext with the given key using AES in GCM mode. should be replaced.
|
||||
func AESGCMDecrypt(key, ciphertext []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size := gcm.NonceSize()
|
||||
if len(ciphertext)-size <= 0 {
|
||||
return nil, errors.New("ciphertext is empty")
|
||||
}
|
||||
|
||||
nonce := ciphertext[:size]
|
||||
ciphertext = ciphertext[size:]
|
||||
|
||||
plainText, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plainText, nil
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -37,21 +35,3 @@ func TestCopyFile(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, testContent, dstContent)
|
||||
}
|
||||
|
||||
func TestAESGCM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, aes.BlockSize)
|
||||
_, err := rand.Read(key)
|
||||
assert.NoError(t, err)
|
||||
|
||||
plaintext := []byte("this will be encrypted")
|
||||
|
||||
ciphertext, err := AESGCMEncrypt(key, plaintext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
decrypted, err := AESGCMDecrypt(key, ciphertext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, plaintext, decrypted)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue