diff --git a/cmd/generate.go b/cmd/generate.go index 35c77a815b..aaf1fe7020 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -6,10 +6,14 @@ package cmd import ( + "encoding/base64" "fmt" "os" "code.gitea.io/gitea/modules/generate" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/secrets" "github.com/mattn/go-isatty" "github.com/urfave/cli" @@ -32,6 +36,7 @@ var ( microcmdGenerateInternalToken, microcmdGenerateLfsJwtSecret, microcmdGenerateSecretKey, + microcmdGenerateMasterKey, }, } @@ -53,6 +58,12 @@ var ( Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } + + microcmdGenerateMasterKey = cli.Command{ + Name: "MASTER_KEY", + Usage: "Generate a new MASTER_KEY", + Action: runGenerateMasterKey, + } ) func runGenerateInternalToken(c *cli.Context) error { @@ -99,3 +110,43 @@ func runGenerateSecretKey(c *cli.Context) error { return nil } + +func runGenerateMasterKey(c *cli.Context) error { + // Silence the console logger + log.DelNamedLogger("console") + log.DelNamedLogger(log.DEFAULT) + + // Read configuration file + setting.LoadFromExisting() + + providerType := secrets.MasterKeyProviderType(setting.MasterKeyProvider) + if providerType == secrets.MasterKeyProviderTypeNone { + return fmt.Errorf("configured master key provider does not support key generation") + } + + if err := secrets.Init(); err != nil { + return err + } + + scrts, err := secrets.GenerateMasterKey() + if err != nil { + return err + } + + if len(scrts) > 1 { + fmt.Println("Unseal secrets:") + for i, secret := range scrts { + if i > 0 { + fmt.Printf("\n") + } + fmt.Printf("%s\n", base64.StdEncoding.EncodeToString(secret)) + } + } + fmt.Println("Setting changes required:") + fmt.Println("[secrets]") + if providerType == secrets.MasterKeyProviderTypePlain && len(scrts) == 1 { + fmt.Printf("MASTER_KEY = %s\n", base64.StdEncoding.EncodeToString(scrts[0])) + } + + return nil +} diff --git a/modules/generate/generate.go b/modules/generate/generate.go index 326fe8036b..c28b99d552 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -9,6 +9,7 @@ import ( "crypto/rand" "encoding/base64" "io" + "math/big" "time" "code.gitea.io/gitea/modules/util" @@ -67,3 +68,23 @@ func NewSecretKey() (string, error) { return secretKey, nil } + +// NewMasterKey generate a new value intended to be used by MASTER_KEY. +func NewMasterKey() ([]byte, error) { + secretBytes := make([]byte, 32) + _, err := io.ReadFull(rand.Reader, secretBytes) + if err != nil { + return nil, err + } + + return secretBytes, nil +} + +func randomInt(max *big.Int) (int, error) { + rand, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + + return int(rand.Int64()), nil +} diff --git a/modules/setting/setting.go b/modules/setting/setting.go index fd2d4b8a58..8b472b42b4 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -215,6 +215,8 @@ var ( HMACKey string `ini:"HMAC_KEY"` Allways bool }{} + MasterKeyProvider string + MasterKey []byte // UI settings UI = struct { @@ -964,6 +966,20 @@ func loadFromConf(allowEmpty bool, extraConfig string) { PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) + // Master key provider configuration + MasterKeyProvider = sec.Key("MASTER_KEY_PROVIDER").MustString("none") + switch MasterKeyProvider { + case "plain": + if MasterKey, err = base64.StdEncoding.DecodeString(sec.Key("MASTER_KEY").MustString("")); err != nil { + log.Fatal("error loading master key: %v", err) + return + } + case "none": + default: + log.Fatal("invalid master key provider type: %v", MasterKeyProvider) + return + } + InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN") if InstallLock && InternalToken == "" { // if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index cced9717be..613aefd411 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -79,6 +79,11 @@ func GetInclude(field reflect.StructField) string { return getRuleBody(field, "Include(") } +// GetIn get allowed values in form tag +func GetIn(field reflect.StructField) string { + return getRuleBody(field, "In(") +} + // Validate validate TODO: func Validate(errs binding.Errors, data map[string]interface{}, f Form, l translation.Locale) binding.Errors { if errs.Len() == 0 { @@ -131,6 +136,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message) case binding.ERR_INCLUDE: data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field)) + case binding.ERR_IN: + data["ErrorMsg"] = trName + l.Tr("form.in_error", strings.Join(strings.Split(GetIn(field), ","), ", ")) case validation.ErrGlobPattern: data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message) case validation.ErrRegexPattern: diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 138ceec345..2ef8b181d0 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -185,6 +185,12 @@ app_url_helper = Base address for HTTP(S) clone URLs and email notifications. log_root_path = Log Path log_root_path_helper = Log files will be written to this directory. +security_title = Security Settings +master_key_provider = Master Key Provider +master_key_provider_none = None +master_key_provider_plain = Plain +master_key_provider_helper = Master Key Provider to use to store secret key that will be used for other secret encryption. Use "None" to not encrypt secrets. Use "Plain" to store automatically generated secret in configuration file. + optional_title = Optional Settings email_title = Email Settings smtp_addr = SMTP Host @@ -245,6 +251,7 @@ password_algorithm = Password Hash Algorithm password_algorithm_helper = Set the password hashing algorithm. Algorithms have differing requirements and strength. `argon2` whilst having good characteristics uses a lot of memory and may be inappropriate for small systems. enable_update_checker = Enable Update Checker enable_update_checker_helper = Checks for new version releases periodically by connecting to gitea.io. +master_key_failed = Failed to generate master key: %v [home] uname_holder = Username or Email Address @@ -466,6 +473,7 @@ max_size_error = ` must contain at most %s characters.` email_error = ` is not a valid email address.` url_error = `'%s' is not a valid URL.` include_error = ` must contain substring '%s'.` +in_error = ` can contain only specific values: %s.` glob_pattern_error = ` glob pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` diff --git a/routers/install/install.go b/routers/install/install.go index 184dc5bae1..29f7be048b 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -7,6 +7,7 @@ package install import ( goctx "context" + "encoding/base64" "fmt" "net/http" "os" @@ -34,6 +35,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/secrets" "gitea.com/go-chi/session" "gopkg.in/ini.v1" @@ -163,6 +165,7 @@ func Install(ctx *context.Context) { form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.NoReplyAddress = setting.Service.NoReplyAddress form.PasswordAlgorithm = setting.PasswordHashAlgo + form.MasterKeyProvider = secrets.MasterKeyProviderTypePlain middleware.AssignForm(form, ctx.Data) ctx.HTML(http.StatusOK, tplInstall) @@ -388,10 +391,40 @@ func SubmitInstall(ctx *context.Context) { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) } } + + // Setup master key provider + cfg.Section("security").Key("MASTER_KEY_PROVIDER").SetValue(string(form.MasterKeyProvider)) + var provider secrets.MasterKeyProvider + switch form.MasterKeyProvider { + case secrets.MasterKeyProviderTypePlain: + provider = secrets.NewPlainMasterKeyProvider() + } + var masterKey []byte + if provider != nil { + if err = provider.Init(); err != nil { + ctx.RenderWithErr(ctx.Tr("install.master_key_failed", err), tplInstall, &form) + return + } + // Generate master key + if _, err = provider.GenerateMasterKey(); err != nil { + ctx.RenderWithErr(ctx.Tr("install.master_key_failed", err), tplInstall, &form) + return + } + masterKey, err = provider.GetMasterKey() + if err != nil { + ctx.RenderWithErr(ctx.Tr("install.master_key_failed", err), tplInstall, &form) + return + } + if form.MasterKeyProvider == secrets.MasterKeyProviderTypePlain { + cfg.Section("security").Key("MASTER_KEY").SetValue(base64.StdEncoding.EncodeToString(masterKey)) + } + } + cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("USER").SetValue(setting.Database.User) + // TODO: Encrypt secret cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd) cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema) cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode) @@ -433,6 +466,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort) cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom) cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser) + // TODO: Encrypt secret cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd) } else { cfg.Section("mailer").Key("ENABLED").SetValue("false") diff --git a/services/forms/user_form.go b/services/forms/user_form.go index da30ae94d6..7c063945ad 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/secrets" "gitea.com/go-chi/binding" ) @@ -64,6 +65,7 @@ type InstallForm struct { NoReplyAddress string PasswordAlgorithm string + MasterKeyProvider secrets.MasterKeyProviderType `binding:"Required;In(none,plain)"` AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"` AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"` diff --git a/services/secrets/masterkey.go b/services/secrets/masterkey.go new file mode 100644 index 0000000000..c69a75b78a --- /dev/null +++ b/services/secrets/masterkey.go @@ -0,0 +1,27 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package secrets + +import ( + "fmt" +) + +// ErrMasterKeySealed is returned when trying to use master key that is sealed +var ErrMasterKeySealed = fmt.Errorf("master key sealed") + +// MasterKeyProvider provides master key used for encryption +type MasterKeyProvider interface { + Init() error + + GenerateMasterKey() ([][]byte, error) + + Unseal(secret []byte) error + + Seal() error + + IsSealed() bool + + GetMasterKey() ([]byte, error) +} diff --git a/services/secrets/masterkey_nop.go b/services/secrets/masterkey_nop.go new file mode 100644 index 0000000000..83d85ab276 --- /dev/null +++ b/services/secrets/masterkey_nop.go @@ -0,0 +1,43 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package secrets + +type nopMasterKeyProvider struct { +} + +// NewNopMasterKeyProvider returns master key provider that holds no master key and is always unsealed +func NewNopMasterKeyProvider() MasterKeyProvider { + return &nopMasterKeyProvider{} +} + +// Init initializes master key provider +func (k *nopMasterKeyProvider) Init() error { + return nil +} + +// GenerateMasterKey always returns empty master key +func (k *nopMasterKeyProvider) GenerateMasterKey() ([][]byte, error) { + return nil, nil +} + +// Unseal master key by providing unsealing secret +func (k *nopMasterKeyProvider) Unseal(secret []byte) error { + return nil +} + +// Seal master key +func (k *nopMasterKeyProvider) Seal() error { + return nil +} + +// IsSealed always returns false +func (k *nopMasterKeyProvider) IsSealed() bool { + return false +} + +// GetMasterKey returns empty master key +func (k *nopMasterKeyProvider) GetMasterKey() ([]byte, error) { + return nil, nil +} diff --git a/services/secrets/masterkey_plain.go b/services/secrets/masterkey_plain.go new file mode 100644 index 0000000000..6a457c745a --- /dev/null +++ b/services/secrets/masterkey_plain.go @@ -0,0 +1,59 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package secrets + +import ( + "code.gitea.io/gitea/modules/generate" + "code.gitea.io/gitea/modules/setting" +) + +type plainMasterKeyProvider struct { + key []byte +} + +// NewPlainMasterKeyProvider returns unsecured static master key provider +func NewPlainMasterKeyProvider() MasterKeyProvider { + return &plainMasterKeyProvider{} +} + +// Init initializes master key provider +func (k *plainMasterKeyProvider) Init() error { + return k.Unseal(nil) +} + +// GenerateMasterKey generates a new master key and returns secret or secrets for unsealing +func (k *plainMasterKeyProvider) GenerateMasterKey() ([][]byte, error) { + key, err := generate.NewMasterKey() + if err != nil { + return nil, err + } + k.key = key + return [][]byte{key}, nil +} + +// Unseal master key by providing unsealing secret +func (k *plainMasterKeyProvider) Unseal(secret []byte) error { + k.key = setting.MasterKey + return nil +} + +// Seal master key +func (k *plainMasterKeyProvider) Seal() error { + k.key = nil + return nil +} + +// IsSealed returns if master key is sealed +func (k *plainMasterKeyProvider) IsSealed() bool { + return len(k.key) == 0 +} + +// GetMasterKey returns master key +func (k *plainMasterKeyProvider) GetMasterKey() ([]byte, error) { + if k.IsSealed() { + return nil, ErrMasterKeySealed + } + return k.key, nil +} diff --git a/services/secrets/secrets.go b/services/secrets/secrets.go new file mode 100644 index 0000000000..accaabea2c --- /dev/null +++ b/services/secrets/secrets.go @@ -0,0 +1,42 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package secrets + +import ( + "fmt" + + "code.gitea.io/gitea/modules/setting" +) + +// MasterKeyProviderType is the type of master key provider +type MasterKeyProviderType string + +// Types of master key providers +const ( + MasterKeyProviderTypeNone MasterKeyProviderType = "none" + MasterKeyProviderTypePlain MasterKeyProviderType = "plain" +) + +var ( + masterKey MasterKeyProvider +) + +// Init initializes master key provider based on settings +func Init() error { + switch MasterKeyProviderType(setting.MasterKeyProvider) { + case MasterKeyProviderTypeNone: + masterKey = NewNopMasterKeyProvider() + case MasterKeyProviderTypePlain: + masterKey = NewPlainMasterKeyProvider() + default: + return fmt.Errorf("invalid master key provider %v", setting.MasterKeyProvider) + } + return nil +} + +// GenerateMasterKey generates a new master key and returns secret or secrets for unsealing +func GenerateMasterKey() ([][]byte, error) { + return masterKey.GenerateMasterKey() +} diff --git a/templates/install.tmpl b/templates/install.tmpl index 0625f43cc4..6d9c69a0d7 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -170,6 +170,22 @@ {{.locale.Tr "install.enable_update_checker_helper"}} + +

{{.i18n.Tr "install.security_title"}}

+ +
+ + + {{.i18n.Tr "install.master_key_provider_helper"}} +

{{.locale.Tr "install.optional_title"}}