| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package secrets | ||
|
|
||
| // EncryptionProvider encrypts and decrypts secrets | ||
| type EncryptionProvider interface { | ||
| Encrypt(secret, key []byte) ([]byte, error) | ||
|
|
||
| EncryptString(secret string, key []byte) (string, error) | ||
|
|
||
| Decrypt(enc, key []byte) ([]byte, error) | ||
|
|
||
| DecryptString(enc string, key []byte) (string, error) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package secrets | ||
|
|
||
| import ( | ||
| "crypto/aes" | ||
| "crypto/cipher" | ||
| "crypto/rand" | ||
| "encoding/base64" | ||
| "fmt" | ||
| "io" | ||
| ) | ||
|
|
||
| type aesEncryptionProvider struct{} | ||
|
|
||
| func NewAesEncryptionProvider() EncryptionProvider { | ||
| return &aesEncryptionProvider{} | ||
| } | ||
|
|
||
| func (e *aesEncryptionProvider) Encrypt(secret, key []byte) ([]byte, error) { | ||
| block, err := aes.NewCipher(key) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| c, err := cipher.NewGCM(block) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| nonce := make([]byte, c.NonceSize(), c.NonceSize()+c.Overhead()+len(secret)) | ||
| if _, err = io.ReadFull(rand.Reader, nonce); err != nil { | ||
| return nil, err | ||
| } | ||
| out := c.Seal(nil, nonce, secret, nil) | ||
|
|
||
| return append(nonce, out...), nil | ||
| } | ||
|
|
||
| func (e *aesEncryptionProvider) EncryptString(secret string, key []byte) (string, error) { | ||
| out, err := e.Encrypt([]byte(secret), key) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return base64.StdEncoding.EncodeToString(out), nil | ||
| } | ||
|
|
||
| func (e *aesEncryptionProvider) Decrypt(enc, key []byte) ([]byte, error) { | ||
| block, err := aes.NewCipher(key) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| c, err := cipher.NewGCM(block) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(enc) < c.NonceSize() { | ||
| return nil, fmt.Errorf("encrypted value has length %d, which is too short for expected %d", len(enc), c.NonceSize()) | ||
| } | ||
|
|
||
| nonce := enc[:c.NonceSize()] | ||
| ciphertext := enc[c.NonceSize():] | ||
|
|
||
| out, err := c.Open(nil, nonce, ciphertext, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return out, nil | ||
| } | ||
|
|
||
| func (e *aesEncryptionProvider) DecryptString(enc string, key []byte) (string, error) { | ||
| encb, err := base64.StdEncoding.DecodeString(enc) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| out, err := e.Decrypt(encb, key) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return string(out), nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package secrets | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestEncryptDecrypt(t *testing.T) { | ||
| provider := NewAesEncryptionProvider() | ||
| key := []byte("1111111111111111") | ||
| pri := "vvvvvvv" | ||
| enc, err := provider.EncryptString(pri, key) | ||
| assert.NoError(t, err) | ||
| v, err := provider.DecryptString(enc, key) | ||
| assert.NoError(t, err) | ||
| assert.EqualValues(t, pri, v) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| 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) | ||
|
There was a problem hiding this comment. Will there ever be providers which return multiple secrets? If yes, why does the There was a problem hiding this comment. Yes there could be for example using Shamir's Secret Sharing (https://github.com/lafriks/go-shamir) where master key would be split to multiple parts. I initially wanted to add this also but that would require too many changes so I left it out. It would require gitea to start in unsealed state (similar to install lock = false state) and would switch to normal state only after X parts of Y part secrets are provided to it.
There was a problem hiding this comment. Will this system replace the There was a problem hiding this comment. SECRET_KEY could be stored encrypted by MASTER_KEY in the future when we add server-wide secret storage we could than use "internal" secrets that would be protected by master key There was a problem hiding this comment. Only secrets we can not protect even in future by master key are There was a problem hiding this comment.
Perhaps you should document that inside the function signature to not confuse future developers. |
||
|
|
||
| Unseal(secret []byte) error | ||
|
|
||
| Seal() error | ||
|
Comment on lines
+19
to
+21
There was a problem hiding this comment. Please write a comment on what these methods are supposed to do… |
||
|
|
||
| IsSealed() bool | ||
|
|
||
| GetMasterKey() ([]byte, error) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package secrets | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestNopMasterKey_IsSealed(t *testing.T) { | ||
| k := NewNopMasterKeyProvider() | ||
| assert.False(t, k.IsSealed()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||
|
There was a problem hiding this comment. Do we copyright the year the code was written, or the year it is merged? |
||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package secrets | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| auth_model "code.gitea.io/gitea/models/auth" | ||
| "code.gitea.io/gitea/models/db" | ||
| "code.gitea.io/gitea/modules/setting" | ||
|
|
||
| "xorm.io/builder" | ||
| ) | ||
|
|
||
| // 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 | ||
| encProvider EncryptionProvider | ||
| ) | ||
|
|
||
| // 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) | ||
| } | ||
|
|
||
| if err := masterKey.Init(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| encProvider = NewAesEncryptionProvider() | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // GenerateMasterKey generates a new master key and returns secret or secrets for unsealing | ||
| func GenerateMasterKey() ([][]byte, error) { | ||
| return masterKey.GenerateMasterKey() | ||
| } | ||
|
|
||
| func Encrypt(secret []byte) ([]byte, error) { | ||
| key, err := masterKey.GetMasterKey() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(key) == 0 { | ||
| return secret, nil | ||
| } | ||
|
|
||
| return encProvider.Encrypt(secret, key) | ||
| } | ||
|
|
||
| func EncryptString(secret string) (string, error) { | ||
| key, err := masterKey.GetMasterKey() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| if len(key) == 0 { | ||
| return secret, nil | ||
| } | ||
|
|
||
| return encProvider.EncryptString(secret, key) | ||
| } | ||
|
|
||
| func Decrypt(enc []byte) ([]byte, error) { | ||
| key, err := masterKey.GetMasterKey() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(key) == 0 { | ||
| return enc, nil | ||
| } | ||
|
|
||
| return encProvider.Decrypt(enc, key) | ||
| } | ||
|
|
||
| func DecryptString(enc string) (string, error) { | ||
| key, err := masterKey.GetMasterKey() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| if len(key) == 0 { | ||
| return enc, nil | ||
| } | ||
|
|
||
| return encProvider.DecryptString(enc, key) | ||
| } | ||
|
|
||
| func InsertRepoSecret(ctx context.Context, repoID int64, key, data string) error { | ||
| v, err := EncryptString(data) | ||
|
There was a problem hiding this comment. I think I've also mentioned it above, but here it is again: |
||
| if err != nil { | ||
| return err | ||
| } | ||
| return db.Insert(ctx, &auth_model.Secret{ | ||
| RepoID: repoID, | ||
| Name: key, | ||
| Data: v, | ||
| }) | ||
| } | ||
|
|
||
| func InsertOwnerSecret(ctx context.Context, ownerID int64, key, data string) error { | ||
| v, err := EncryptString(data) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return db.Insert(ctx, &auth_model.Secret{ | ||
| OwnerID: ownerID, | ||
| Name: key, | ||
| Data: v, | ||
| }) | ||
| } | ||
|
|
||
| func DeleteSecretByID(ctx context.Context, id int64) error { | ||
| _, err := db.DeleteByBean(ctx, &auth_model.Secret{ID: id}) | ||
| return err | ||
| } | ||
|
|
||
| func DeleteSecretsByRepoID(ctx context.Context, repoID int64) error { | ||
| _, err := db.DeleteByBean(ctx, &auth_model.Secret{RepoID: repoID}) | ||
| return err | ||
| } | ||
|
|
||
| func DeleteSecretsByOwnerID(ctx context.Context, ownerID int64) error { | ||
| _, err := db.DeleteByBean(ctx, &auth_model.Secret{ID: ownerID}) | ||
| return err | ||
| } | ||
|
|
||
| func FindRepoSecrets(ctx context.Context, repoID int64) ([]*auth_model.Secret, error) { | ||
| var res []*auth_model.Secret | ||
| return res, db.GetEngine(ctx).Where(builder.Eq{"repo_id": repoID}).Find(&res) | ||
| } | ||
|
|
||
| func FindOwnerSecrets(ctx context.Context, ownerID int64) ([]*auth_model.Secret, error) { | ||
| var res []*auth_model.Secret | ||
| return res, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).Find(&res) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| {{template "base/head" .}} | ||
| <div class="page-content organization settings webhooks"> | ||
| {{template "org/header" .}} | ||
| <div class="ui container"> | ||
| <div class="ui grid"> | ||
| {{template "org/settings/navbar" .}} | ||
| <div class="ui twelve wide column content"> | ||
| {{template "base/alert" .}} | ||
| <h4 class="ui top attached header"> | ||
| {{.locale.Tr "repo.settings.secrets"}} | ||
| <div class="ui right"> | ||
| <div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "repo.settings.add_secret"}}</div> | ||
| </div> | ||
| </h4> | ||
| <div class="ui attached segment"> | ||
| <div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel"> | ||
| <form class="ui form" action="{{.Link}}?act=secret" method="post"> | ||
| {{.CsrfTokenHtml}} | ||
| <div class="field"> | ||
| {{.locale.Tr "repo.settings.secret_desc"}} | ||
| </div> | ||
| <div class="field {{if .Err_Title}}error{{end}}"> | ||
| <label for="title">{{.locale.Tr "repo.settings.secret_name"}}</label> | ||
| <input id="key-title" name="title" value="{{.title}}" autofocus required> | ||
| </div> | ||
| <div class="field {{if .Err_Content}}error{{end}}"> | ||
| <label for="content">{{.locale.Tr "repo.settings.secret_content"}}</label> | ||
| <textarea id="key-content" name="content" placeholder="{{.locale.Tr "repo.settings.secret_value_content_placeholder"}}" required>{{.content}}</textarea> | ||
| </div> | ||
| <button class="ui green button"> | ||
| {{.locale.Tr "repo.settings.add_secret"}} | ||
| </button> | ||
| <button class="ui hide-panel button" data-panel="#add-secret-panel"> | ||
| {{.locale.Tr "cancel"}} | ||
| </button> | ||
| </form> | ||
| </div> | ||
| {{if .Secrets}} | ||
| <div class="ui key list"> | ||
| {{range .Secrets}} | ||
| <div class="item"> | ||
| <div class="right floated content"> | ||
| <button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}"> | ||
| {{$.locale.Tr "settings.delete_key"}} | ||
| </button> | ||
| </div> | ||
| <div class="left floated content"> | ||
| <i class="tooltip">{{svg "octicon-key" 32}}</i> | ||
| </div> | ||
| <div class="content"> | ||
| <strong>{{.Name}}</strong> | ||
| <div class="print meta"> | ||
| {{Shadow .Data}} | ||
| </div> | ||
| <div class="activity meta"> | ||
| <i> | ||
| {{$.locale.Tr "settings.add_on"}} | ||
| <span>{{.CreatedUnix.FormatShort}}</span> | ||
| </i> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}} | ||
| </div> | ||
| {{else}} | ||
| {{.locale.Tr "repo.settings.no_secret"}} | ||
| {{end}} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="ui small basic delete modal"> | ||
| <div class="ui header"> | ||
| {{svg "octicon-trash" 16 "mr-2"}} | ||
| {{.locale.Tr "repo.settings.secret_deletion"}} | ||
| </div> | ||
|
Comment on lines
+75
to
+78
There was a problem hiding this comment. I think that can be deleted as it doesn't really look good. |
||
| <div class="content"> | ||
| <p>{{.locale.Tr "repo.settings.secret_deletion_desc"}}</p> | ||
| </div> | ||
| {{template "base/delete_modal_actions" .}} | ||
| </div> | ||
|
|
||
| {{template "base/footer" .}} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <div class="ui container"> | ||
| <h4 class="ui top attached header"> | ||
| {{.locale.Tr "repo.settings.secrets"}} | ||
| <div class="ui right"> | ||
| <div class="ui primary tiny show-panel button" data-panel="#add-secret-panel">{{.locale.Tr "repo.settings.add_secret"}}</div> | ||
| </div> | ||
| </h4> | ||
| <div class="ui attached segment"> | ||
| <div class="{{if not .HasError}}hide {{end}}mb-4" id="add-secret-panel"> | ||
| <form class="ui form" action="{{.Link}}?act=secret" method="post"> | ||
| {{.CsrfTokenHtml}} | ||
| <div class="field"> | ||
| {{.locale.Tr "repo.settings.secret_desc"}} | ||
| </div> | ||
| <div class="field {{if .Err_Title}}error{{end}}"> | ||
| <label for="title">{{.locale.Tr "repo.settings.secret_name"}}</label> | ||
| <input id="ssh-key-title" name="title" value="{{.title}}" autofocus required> | ||
| </div> | ||
| <div class="field {{if .Err_Content}}error{{end}}"> | ||
| <label for="content">{{.locale.Tr "repo.settings.secret_content"}}</label> | ||
| <textarea id="ssh-key-content" name="content" placeholder="{{.locale.Tr "repo.settings.secret_value_content_placeholder"}}" required>{{.content}}</textarea> | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
| <button class="ui green button"> | ||
| {{.locale.Tr "repo.settings.add_secret"}} | ||
| </button> | ||
| <button class="ui hide-panel button" data-panel="#add-secret-panel"> | ||
| {{.locale.Tr "cancel"}} | ||
| </button> | ||
| </form> | ||
| </div> | ||
| {{if .Secrets}} | ||
| <div class="ui key list"> | ||
| {{range .Secrets}} | ||
| <div class="item"> | ||
| <div class="right floated content"> | ||
| <button class="ui red tiny button delete-button" data-url="{{$.Link}}/delete?act=secret" data-id="{{.ID}}"> | ||
| {{$.locale.Tr "settings.delete_key"}} | ||
| </button> | ||
| </div> | ||
| <div class="left floated content"> | ||
| <i class="tooltip">{{svg "octicon-key" 32}}</i> | ||
|
There was a problem hiding this comment. tooltip? And why italic? Wouldn't the svg suffice? |
||
| </div> | ||
| <div class="content"> | ||
| <strong>{{.Name}}</strong> | ||
| <div class="print meta"> | ||
| {{Shadow .Data}} | ||
| </div> | ||
| <div class="activity meta"> | ||
| <i> | ||
| {{$.locale.Tr "settings.add_on"}} | ||
| <span>{{.CreatedUnix.FormatShort}}</span> | ||
| </i> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {{end}} | ||
| </div> | ||
| {{else}} | ||
| {{.locale.Tr "repo.settings.no_secret"}} | ||
| {{end}} | ||
| </div> | ||
| </div> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?