Secrets encryption in database (#1475)

closes #101

Added secrets encryption in database

- Google TINK or simple AES as encryption mechanisms
- Keys rotation support on TINK
- Existing SecretService is wrapped by encryption layer
- Encryption can be enabled and disabled at any time

Co-authored-by: Kuzmin Ilya <ilia.kuzmin@indrive.com>
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
antomy-gc 2023-01-12 22:59:07 +03:00 committed by GitHub
parent f71142d162
commit 6516a28cdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1633 additions and 8 deletions

View file

@ -525,4 +525,23 @@ var flags = []cli.Flag{
Hidden: true,
// TODO(485) temporary workaround to not hit api rate limits
},
//
// secrets encryption in DB
//
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_KEY"},
Name: "encryption-raw-key",
Usage: "Raw encryption key",
FilePath: os.Getenv("WOODPECKER_ENCRYPTION_KEY_FILE"),
},
&cli.StringFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE"},
Name: "encryption-tink-keyset",
Usage: "Google tink AEAD-compatible keyset file to encrypt secrets in DB",
},
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_ENCRYPTION_DISABLE"},
Name: "encryption-disable-flag",
Usage: "Flag to decrypt all encrypted data and disable encryption on server",
},
}

View file

@ -44,6 +44,8 @@ import (
"github.com/woodpecker-ci/woodpecker/server/logging"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
"github.com/woodpecker-ci/woodpecker/server/plugins/encryption"
encryptedStore "github.com/woodpecker-ci/woodpecker/server/plugins/encryption/wrapper/store"
"github.com/woodpecker-ci/woodpecker/server/pubsub"
"github.com/woodpecker-ci/woodpecker/server/router"
"github.com/woodpecker-ci/woodpecker/server/router/middleware"
@ -260,6 +262,13 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
// forge
server.Config.Services.Forge = f
// encryption
encryptedSecretStore := encryptedStore.NewSecretStore(v)
err := encryption.Encryption(c, v).WithClient(encryptedSecretStore).Build()
if err != nil {
log.Fatal().Err(err).Msg("could not create encryption service")
}
// services
server.Config.Services.Queue = setupQueue(c, v)
server.Config.Services.Logs = logging.New()
@ -268,7 +277,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
log.Error().Err(err).Msg("could not create pubsub service")
}
server.Config.Services.Registries = setupRegistryService(c, v)
server.Config.Services.Secrets = setupSecretService(c, v)
server.Config.Services.Secrets = setupSecretService(c, encryptedSecretStore)
server.Config.Services.Environ = setupEnvironService(c, v)
server.Config.Services.Membership = setupMembershipService(c, f)

View file

@ -164,7 +164,7 @@ func setupQueue(c *cli.Context, s store.Store) queue.Queue {
return queue.WithTaskStore(queue.New(c.Context), s)
}
func setupSecretService(c *cli.Context, s store.Store) model.SecretService {
func setupSecretService(c *cli.Context, s model.SecretStore) model.SecretService {
return secrets.New(c.Context, s)
}

View file

@ -290,6 +290,26 @@ WOODPECKER_DATABASE_DATASOURCE=postgres://root:password@1.2.3.4:5432/woodpecker?
Read the value for `WOODPECKER_DATABASE_DATASOURCE` from the specified filepath
### `WOODPECKER_ENCRYPTION_KEY`
> Default: empty
Encryption key used to encrypt secrets in DB. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_ENCRYPTION_KEY_FILE`
> Default: empty
Read the value for `WOODPECKER_ENCRYPTION_KEY` from the specified filepath
### `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE`
> Default: empty
Filepath to encryption keyset used to encrypt secrets in DB. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_ENCRYPTION_DISABLE`
> Default: empty
Boolean flag to decrypt secrets in DB and disable server encryption. See [secrets encryption](./40-encryption.md)
### `WOODPECKER_PROMETHEUS_AUTH_TOKEN`
> Default: empty

View file

@ -0,0 +1,64 @@
# Secrets encryption
By default, Woodpecker does not encrypt secrets in its database. You can enable encryption
using simple AES key or more advanced [Google TINK](https://developers.google.com/tink) encryption.
## Common
### Enabling secrets encryption
To enable secrets encryption and encrypt all existing secrets in database set
`WOODPECKER_ENCRYPTION_KEY`, `WOODPECKER_ENCRYPTION_KEY_FILE` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_PATH` environment
variable depending on encryption method of your choice.
After encryption is enabled you will be unable to start Woodpecker server without providing valid encryption key!
### Disabling encryption and decrypting all secrets
To disable secrets encryption and decrypt database you need to start server with valid
`WOODPECKER_ENCRYPTION_KEY` or `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` environment variable set depending on
enabled encryption method, and `WOODPECKER_ENCRYPTION_DISABLE` set to true.
After secrets was decrypted server will proceed working in unencrypted mode. You will not need to use "disable encryption"
variable or encryption keys to start server anymore.
## AES
Simple AES encryption.
### Configuration
You can manage encryption on server using these environment variables:
- `WOODPECKER_ENCRYPTION_KEY` - encryption key
- `WOODPECKER_ENCRYPTION_KEY_FILE` - file to read encryption key from
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server
## TINK
TINK uses AEAD encryption instead of simple AES and supports key rotation.
### Configuration
You can manage encryption on server using these two environment variables:
- `WOODPECKER_ENCRYPTION_TINK_KEYSET_FILE` - keyset filepath
- `WOODPECKER_ENCRYPTION_DISABLE` - disable encryption flag used to decrypt all data on server
### Encryption keys
You will need plaintext AEAD-compatible Google TINK keyset to encrypt your data.
To generate it and then rotate keys if needed, install `tinkey`([installation guide](https://developers.google.com/tink/install-tinkey))
Keyset contains one or more keys, used to encrypt or decrypt your data, and primary key ID, used to determine which key
to use while encrypting new data.
Keyset generation example:
```shell
tinkey create-keyset --key-template AES256_GCM --out-format json --out keyset.json
```
### Key rotation
Use `tinkey` to rotate encryption keys in your existing keyset:
```shell
tinkey rotate-keyset --in keyset_v1.json --out keyset_v2.json --key-template AES256_GCM
```
Then you just need to replace server keyset file with the new one. At the moment server detects new encryption
keyset it will re-encrypt all existing secrets with the new key, so you will be unable to start server with previous
keyset anymore.

5
go.mod
View file

@ -15,12 +15,14 @@ require (
github.com/docker/go-units v0.5.0
github.com/drone/envsubst v1.0.3
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
github.com/fsnotify/fsnotify v1.5.1
github.com/gin-gonic/gin v1.8.1
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51
github.com/go-sql-driver/mysql v1.6.0
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/go-github/v39 v39.2.0
github.com/google/tink/go v1.7.0
github.com/gorilla/securecookie v1.1.1
github.com/joho/godotenv v1.4.0
github.com/lafriks/ttlcache/v3 v3.2.0
@ -39,6 +41,7 @@ require (
github.com/urfave/cli/v2 v2.20.2
github.com/xanzy/go-gitlab v0.73.1
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.1.0
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
golang.org/x/net v0.4.0
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
@ -66,7 +69,6 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
@ -127,7 +129,6 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect

2
go.sum
View file

@ -298,6 +298,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w=
github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=

View file

@ -0,0 +1,41 @@
// Copyright 2022 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
// EncryptionBuilder is user API to obtain correctly configured encryption
type EncryptionBuilder interface {
WithClient(client EncryptionClient) EncryptionBuilder
Build() error
}
type EncryptionServiceBuilder interface {
WithClients(clients []EncryptionClient) EncryptionServiceBuilder
Build() (EncryptionService, error)
}
type EncryptionService interface {
Encrypt(plaintext, associatedData string) (string, error)
Decrypt(ciphertext, associatedData string) (string, error)
Disable() error
}
type EncryptionClient interface {
// SetEncryptionService should be used only by EncryptionServiceBuilder
SetEncryptionService(encryption EncryptionService) error
// EnableEncryption should encrypt all service data
EnableEncryption() error
// MigrateEncryption should decrypt all existing data and encrypt it with new encryption service
MigrateEncryption(newEncryption EncryptionService) error
}

View file

@ -63,6 +63,7 @@ type SecretStore interface {
OrgSecretList(string) ([]*Secret, error)
GlobalSecretFind(string) (*Secret, error)
GlobalSecretList() ([]*Secret, error)
SecretListAll() ([]*Secret, error)
}
// Secret represents a secret variable, such as a password or token.

View file

@ -0,0 +1,67 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"crypto/cipher"
"encoding/base64"
"fmt"
"github.com/google/tink/go/subtle/random"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
)
type aesEncryptionService struct {
cipher cipher.AEAD
keyID string
store store.Store
clients []model.EncryptionClient
}
func (svc *aesEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
msg := []byte(plaintext)
aad := []byte(associatedData)
nonce := random.GetRandomBytes(uint32(AESGCMSIVNonceSize))
ciphertext := svc.cipher.Seal(nil, nonce, msg, aad)
result := make([]byte, 0, AESGCMSIVNonceSize+len(ciphertext))
result = append(result, nonce...)
result = append(result, ciphertext...)
return base64.StdEncoding.EncodeToString(result), nil
}
func (svc *aesEncryptionService) Decrypt(ciphertext, associatedData string) (string, error) {
bytes, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf(errTemplateBase64DecryptionFailed, err)
}
nonce := bytes[:AESGCMSIVNonceSize]
message := bytes[AESGCMSIVNonceSize:]
plaintext, err := svc.cipher.Open(nil, nonce, message, []byte(associatedData))
if err != nil {
return "", fmt.Errorf(errTemplateDecryptionFailed, err)
}
return string(plaintext), nil
}
func (svc *aesEncryptionService) Disable() error {
return svc.disable()
}

View file

@ -0,0 +1,66 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
)
type aesConfiguration struct {
password string
store store.Store
clients []model.EncryptionClient
}
func newAES(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder {
key := ctx.String(rawKeyConfigFlag)
return &aesConfiguration{key, s, nil}
}
func (c aesConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
c.clients = clients
return c
}
func (c aesConfiguration) Build() (model.EncryptionService, error) {
svc := &aesEncryptionService{
cipher: nil,
store: c.store,
clients: c.clients,
}
err := svc.initClients()
if err != nil {
return nil, fmt.Errorf(errTemplateFailedInitializingClients, err)
}
err = svc.loadCipher(c.password)
if err != nil {
return nil, fmt.Errorf(errTemplateAesFailedLoadingCipher, err)
}
err = svc.validateKey()
if err == errEncryptionNotEnabled {
err = svc.enable()
}
if err != nil {
return nil, fmt.Errorf(errTemplateFailedValidatingKey, err)
}
return svc, nil
}

View file

@ -0,0 +1,83 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"crypto/aes"
"crypto/cipher"
"errors"
"fmt"
"github.com/woodpecker-ci/woodpecker/server/store/types"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/sha3"
)
func (svc *aesEncryptionService) loadCipher(password string) error {
key, err := svc.hash([]byte(password))
if err != nil {
return fmt.Errorf(errTemplateAesFailedGeneratingKey, err)
}
keyHash, err := bcrypt.GenerateFromPassword(key, bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf(errTemplateAesFailedGeneratingKeyID, err)
}
svc.keyID = string(keyHash)
block, err := aes.NewCipher(key)
if err != nil {
return fmt.Errorf(errTemplateAesFailedLoadingCipher, err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
return fmt.Errorf(errTemplateAesFailedLoadingCipher, err)
}
svc.cipher = aead
return nil
}
func (svc *aesEncryptionService) validateKey() error {
ciphertextSample, err := svc.store.ServerConfigGet(ciphertextSampleConfigKey)
if errors.Is(err, types.RecordNotExist) {
return errEncryptionNotEnabled
} else if err != nil {
return fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
}
plaintext, err := svc.Decrypt(ciphertextSample, keyIDAssociatedData)
if plaintext != svc.keyID {
return errEncryptionKeyInvalid
} else if err != nil {
return err
}
return nil
}
func (svc *aesEncryptionService) hash(data []byte) ([]byte, error) {
result := make([]byte, 32)
sha := sha3.NewShake256()
_, err := sha.Write(data)
if err != nil {
return nil, fmt.Errorf(errTemplateAesFailedCalculatingHash, err)
}
_, err = sha.Read(result)
if err != nil {
return nil, fmt.Errorf(errTemplateAesFailedCalculatingHash, err)
}
return result, nil
}

View file

@ -0,0 +1,101 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/rs/zerolog/log"
)
func (svc *aesEncryptionService) initClients() error {
for _, client := range svc.clients {
err := client.SetEncryptionService(svc)
if err != nil {
return fmt.Errorf(errTemplateFailedInitializingClients, err)
}
}
log.Info().Msg(logMessageClientsInitialized)
return nil
}
func (svc *aesEncryptionService) enable() error {
err := svc.callbackOnEnable()
if err != nil {
return fmt.Errorf(errTemplateFailedEnablingEncryption, err)
}
err = svc.updateCiphertextSample()
if err != nil {
return fmt.Errorf(errTemplateFailedEnablingEncryption, err)
}
log.Warn().Msg(logMessageEncryptionEnabled)
return nil
}
func (svc *aesEncryptionService) disable() error {
err := svc.callbackOnDisable()
if err != nil {
return fmt.Errorf(errTemplateFailedDisablingEncryption, err)
}
err = svc.deleteCiphertextSample()
if err != nil {
return fmt.Errorf(errTemplateFailedDisablingEncryption, err)
}
log.Warn().Msg(logMessageEncryptionDisabled)
return nil
}
func (svc *aesEncryptionService) updateCiphertextSample() error {
ciphertext, err := svc.Encrypt(svc.keyID, keyIDAssociatedData)
if err != nil {
return fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
err = svc.store.ServerConfigSet(ciphertextSampleConfigKey, ciphertext)
if err != nil {
return fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
log.Info().Msg(logMessageEncryptionKeyRegistered)
return nil
}
func (svc *aesEncryptionService) deleteCiphertextSample() error {
err := svc.store.ServerConfigDelete(ciphertextSampleConfigKey)
if err != nil {
err = fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
return err
}
func (svc *aesEncryptionService) callbackOnEnable() error {
for _, client := range svc.clients {
err := client.EnableEncryption()
if err != nil {
return fmt.Errorf(errTemplateFailedEnablingEncryption, err)
}
}
log.Info().Msg(logMessageClientsEnabled)
return nil
}
func (svc *aesEncryptionService) callbackOnDisable() error {
for _, client := range svc.clients {
err := client.MigrateEncryption(&noEncryption{})
if err != nil {
return fmt.Errorf(errTemplateFailedDisablingEncryption, err)
}
}
log.Info().Msg(logMessageEncryptionDisabled)
return nil
}

View file

@ -0,0 +1,50 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"testing"
"github.com/google/tink/go/subtle/random"
"github.com/stretchr/testify/assert"
)
func TestShortMessageLongKey(t *testing.T) {
aes := &aesEncryptionService{}
err := aes.loadCipher(string(random.GetRandomBytes(32)))
assert.Nil(t, err)
input := string(random.GetRandomBytes(4))
cipher, err := aes.Encrypt(input, "")
assert.Nil(t, err)
output, err := aes.Decrypt(cipher, "")
assert.Nil(t, err)
assert.Equal(t, input, output)
}
func TestLongMessageShortKey(t *testing.T) {
aes := &aesEncryptionService{}
err := aes.loadCipher(string(random.GetRandomBytes(12)))
assert.Nil(t, err)
input := string(random.GetRandomBytes(1024))
cipher, err := aes.Encrypt(input, "")
assert.Nil(t, err)
output, err := aes.Decrypt(cipher, "")
assert.Nil(t, err)
assert.Equal(t, input, output)
}

View file

@ -0,0 +1,99 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import "errors"
// common
const (
rawKeyConfigFlag = "encryption-raw-key"
tinkKeysetFilepathConfigFlag = "encryption-tink-keyset"
disableEncryptionConfigFlag = "encryption-disable-flag"
ciphertextSampleConfigKey = "encryption-ciphertext-sample"
keyTypeTink = "tink"
keyTypeRaw = "raw"
keyTypeNone = "none"
keyIDAssociatedData = "Primary key id"
AESGCMSIVNonceSize = 12
)
var (
errEncryptionNotEnabled = errors.New("encryption is not enabled")
errEncryptionKeyInvalid = errors.New("encryption key is invalid")
errEncryptionKeyRotated = errors.New("encryption key is being rotated")
)
const (
// error wrapping templates
errTemplateFailedInitializingUnencrypted = "failed initializing server in unencrypted mode: %w"
errTemplateFailedInitializing = "failed initializing encryption service: %w"
errTemplateFailedEnablingEncryption = "failed enabling encryption: %w"
errTemplateFailedRotatingEncryption = "failed rotating encryption: %w"
errTemplateFailedDisablingEncryption = "failed disabling encryption: %w"
errTemplateFailedLoadingServerConfig = "failed to load server encryption config: %w"
errTemplateFailedUpdatingServerConfig = "failed updating server encryption configuration: %w"
errTemplateFailedInitializingClients = "failed initializing encryption clients: %w"
errTemplateFailedValidatingKey = "failed validating encryption key: %w"
errTemplateEncryptionFailed = "encryption error: %w"
errTemplateBase64DecryptionFailed = "decryption error: Base64 decryption failed. Cause: %w"
errTemplateDecryptionFailed = "decryption error: %w"
// error messages
errMessageTemplateUnsupportedKeyType = "unsupported encryption key type: %s"
errMessageCantUseBothServices = "can not use raw encryption key and tink keyset at the same time"
errMessageNoKeysProvided = "encryption enabled but no keys provided"
errMessageFailedRotatingEncryption = "failed rotating encryption"
// log messages
logMessageEncryptionEnabled = "encryption enabled"
logMessageEncryptionDisabled = "encryption disabled"
logMessageEncryptionKeyRegistered = "registered new encryption key"
logMessageClientsInitialized = "initialized encryption on registered clients"
logMessageClientsEnabled = "enabled encryption on registered services"
logMessageClientsRotated = "updated encryption key on registered services"
logMessageClientsDecrypted = "disabled encryption on registered services"
)
// tink
const (
// error wrapping templates
errTemplateTinkFailedLoadingKeyset = "failed loading encryption keyset: %w"
errTemplateTinkFailedValidatingKeyset = "failed validating encryption keyset: %w"
errTemplateTinkFailedInitializeFileWatcher = "failed initializing keyset file watcher: %w"
errTemplateTinkFailedSubscribeKeysetFileChanges = "failed subscribing on encryption keyset file changes: %w"
errTemplateTinkFailedOpeningKeyset = "failed opening encryption keyset file: %w"
errTemplateTinkFailedReadingKeyset = "failed reading encryption keyset from file: %w"
errTemplateTinkFailedInitializingAEAD = "failed initializing AEAD instance: %w"
// error messages
errMessageTinkKeysetFileWatchFailed = "failed watching encryption keyset file changes"
// log message templates
logTemplateTinkKeysetFileChanged = "changes detected in encryption keyset file: '%s'. Encryption service will be reloaded"
logTemplateTinkLoadingKeyset = "loading encryption keyset from file: %s"
logTemplateTinkFailedClosingKeysetFile = "could not close keyset file: %s"
)
// aes
const (
// error wrapping templates
errTemplateAesFailedLoadingCipher = "failed loading encryption cipher: %w"
errTemplateAesFailedCalculatingHash = "failed calculating hash: %w"
errTemplateAesFailedGeneratingKey = "failed generating key from passphrase: %w"
errTemplateAesFailedGeneratingKeyID = "failed generating key id: %w"
)

View file

@ -0,0 +1,72 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
)
type builder struct {
store store.Store
ctx *cli.Context
clients []model.EncryptionClient
}
func Encryption(ctx *cli.Context, s store.Store) model.EncryptionBuilder {
return &builder{store: s, ctx: ctx}
}
func (b builder) WithClient(client model.EncryptionClient) model.EncryptionBuilder {
b.clients = append(b.clients, client)
return b
}
func (b builder) Build() error {
enabled, err := b.isEnabled()
if err != nil {
return err
}
disableFlag := b.ctx.Bool(disableEncryptionConfigFlag)
keyType, err := b.detectKeyType()
if err != nil {
return err
}
if !enabled && (disableFlag || keyType == keyTypeNone) {
_, err := noEncryptionBuilder{}.WithClients(b.clients).Build()
if err != nil {
return fmt.Errorf(errTemplateFailedInitializingUnencrypted, err)
}
}
svc, err := b.getService(keyType)
if err != nil {
return fmt.Errorf(errTemplateFailedInitializing, err)
}
if disableFlag {
err := svc.Disable()
if err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,73 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"errors"
"fmt"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store/types"
)
func (b builder) getService(keyType string) (model.EncryptionService, error) {
if keyType == keyTypeNone {
return nil, errors.New(errMessageNoKeysProvided)
}
builder, err := b.serviceBuilder(keyType)
if err != nil {
return nil, err
}
svc, err := builder.WithClients(b.clients).Build()
if err != nil {
return nil, err
}
return svc, nil
}
func (b builder) isEnabled() (bool, error) {
_, err := b.store.ServerConfigGet(ciphertextSampleConfigKey)
if err != nil && !errors.Is(err, types.RecordNotExist) {
return false, fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
}
return err == nil, nil
}
func (b builder) detectKeyType() (string, error) {
rawKeyPresent := b.ctx.IsSet(rawKeyConfigFlag)
tinkKeysetPresent := b.ctx.IsSet(tinkKeysetFilepathConfigFlag)
if rawKeyPresent && tinkKeysetPresent {
return "", errors.New(errMessageCantUseBothServices)
} else if rawKeyPresent {
return keyTypeRaw, nil
} else if tinkKeysetPresent {
return keyTypeTink, nil
}
return keyTypeNone, nil
}
func (b builder) serviceBuilder(keyType string) (model.EncryptionServiceBuilder, error) {
if keyType == keyTypeTink {
return newTink(b.ctx, b.store), nil
} else if keyType == keyTypeRaw {
return newAES(b.ctx, b.store), nil
} else if keyType == keyTypeNone {
return &noEncryptionBuilder{}, nil
} else {
return nil, fmt.Errorf(errMessageTemplateUnsupportedKeyType, keyType)
}
}

View file

@ -0,0 +1,51 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import "github.com/woodpecker-ci/woodpecker/server/model"
type noEncryptionBuilder struct {
clients []model.EncryptionClient
}
func (b noEncryptionBuilder) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
b.clients = clients
return b
}
func (b noEncryptionBuilder) Build() (model.EncryptionService, error) {
svc := &noEncryption{}
for _, client := range b.clients {
err := client.SetEncryptionService(svc)
if err != nil {
return nil, err
}
}
return svc, nil
}
type noEncryption struct{}
func (svc *noEncryption) Encrypt(plaintext, _ string) (string, error) {
return plaintext, nil
}
func (svc *noEncryption) Decrypt(ciphertext, _ string) (string, error) {
return ciphertext, nil
}
func (svc *noEncryption) Disable() error {
return nil
}

View file

@ -0,0 +1,62 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"encoding/base64"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/google/tink/go/tink"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
)
type tinkEncryptionService struct {
keysetFilePath string
primaryKeyID string
encryption tink.AEAD
store store.Store
keysetFileWatcher *fsnotify.Watcher
clients []model.EncryptionClient
}
func (svc *tinkEncryptionService) Encrypt(plaintext, associatedData string) (string, error) {
msg := []byte(plaintext)
aad := []byte(associatedData)
ciphertext, err := svc.encryption.Encrypt(msg, aad)
if err != nil {
return "", fmt.Errorf(errTemplateEncryptionFailed, err)
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func (svc *tinkEncryptionService) Decrypt(ciphertext, associatedData string) (string, error) {
ct, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf(errTemplateBase64DecryptionFailed, err)
}
plaintext, err := svc.encryption.Decrypt(ct, []byte(associatedData))
if err != nil {
return "", fmt.Errorf(errTemplateDecryptionFailed, err)
}
return string(plaintext), nil
}
func (svc *tinkEncryptionService) Disable() error {
return svc.disable()
}

View file

@ -0,0 +1,77 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/woodpecker-ci/woodpecker/server/model"
"github.com/woodpecker-ci/woodpecker/server/store"
)
type tinkConfiguration struct {
keysetFilePath string
store store.Store
clients []model.EncryptionClient
}
func newTink(ctx *cli.Context, s store.Store) model.EncryptionServiceBuilder {
filepath := ctx.String(tinkKeysetFilepathConfigFlag)
return &tinkConfiguration{filepath, s, nil}
}
func (c tinkConfiguration) WithClients(clients []model.EncryptionClient) model.EncryptionServiceBuilder {
c.clients = clients
return c
}
func (c tinkConfiguration) Build() (model.EncryptionService, error) {
svc := &tinkEncryptionService{
keysetFilePath: c.keysetFilePath,
primaryKeyID: "",
encryption: nil,
store: c.store,
keysetFileWatcher: nil,
clients: c.clients,
}
err := svc.initClients()
if err != nil {
return nil, fmt.Errorf(errTemplateFailedInitializingClients, err)
}
err = svc.loadKeyset()
if err != nil {
return nil, fmt.Errorf(errTemplateTinkFailedLoadingKeyset, err)
}
err = svc.validateKeyset()
if err == errEncryptionNotEnabled {
err = svc.enable()
} else if err == errEncryptionKeyRotated {
err = svc.rotate()
}
if err != nil {
return nil, fmt.Errorf(errTemplateTinkFailedValidatingKeyset, err)
}
err = svc.initFileWatcher()
if err != nil {
return nil, fmt.Errorf(errTemplateTinkFailedInitializeFileWatcher, err)
}
return svc, nil
}

View file

@ -0,0 +1,74 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"errors"
"fmt"
"os"
"strconv"
"github.com/google/tink/go/aead"
"github.com/google/tink/go/insecurecleartextkeyset"
"github.com/google/tink/go/keyset"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server/store/types"
)
func (svc *tinkEncryptionService) loadKeyset() error {
log.Warn().Msgf(logTemplateTinkLoadingKeyset, svc.keysetFilePath)
file, err := os.Open(svc.keysetFilePath)
if err != nil {
return fmt.Errorf(errTemplateTinkFailedOpeningKeyset, err)
}
defer func(file *os.File) {
err = file.Close()
if err != nil {
log.Err(err).Msgf(logTemplateTinkFailedClosingKeysetFile, svc.keysetFilePath)
}
}(file)
jsonKeyset := keyset.NewJSONReader(file)
keysetHandle, err := insecurecleartextkeyset.Read(jsonKeyset)
if err != nil {
return fmt.Errorf(errTemplateTinkFailedReadingKeyset, err)
}
svc.primaryKeyID = strconv.FormatUint(uint64(keysetHandle.KeysetInfo().PrimaryKeyId), 10)
encryptionInstance, err := aead.New(keysetHandle)
if err != nil {
return fmt.Errorf(errTemplateTinkFailedInitializingAEAD, err)
}
svc.encryption = encryptionInstance
return nil
}
func (svc *tinkEncryptionService) validateKeyset() error {
ciphertextSample, err := svc.store.ServerConfigGet(ciphertextSampleConfigKey)
if errors.Is(err, types.RecordNotExist) {
return errEncryptionNotEnabled
} else if err != nil {
return fmt.Errorf(errTemplateFailedLoadingServerConfig, err)
}
plaintext, err := svc.Decrypt(ciphertextSample, keyIDAssociatedData)
if plaintext != svc.primaryKeyID {
return errEncryptionKeyRotated
} else if err != nil {
return fmt.Errorf(errTemplateFailedValidatingKey, err)
}
return nil
}

View file

@ -0,0 +1,61 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog/log"
)
// Watch keyset file events to detect key rotations and hot reload keys
func (svc *tinkEncryptionService) initFileWatcher() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf(errTemplateTinkFailedSubscribeKeysetFileChanges, err)
}
err = watcher.Add(svc.keysetFilePath)
if err != nil {
return fmt.Errorf(errTemplateTinkFailedSubscribeKeysetFileChanges, err)
}
svc.keysetFileWatcher = watcher
go svc.handleFileEvents()
return nil
}
func (svc *tinkEncryptionService) handleFileEvents() {
for {
select {
case event, ok := <-svc.keysetFileWatcher.Events:
if !ok {
log.Fatal().Msg(errMessageTinkKeysetFileWatchFailed)
}
if (event.Op == fsnotify.Write) || (event.Op == fsnotify.Create) {
log.Warn().Msgf(logTemplateTinkKeysetFileChanged, event.Name)
err := svc.rotate()
if err != nil {
log.Fatal().Err(err).Msgf(errMessageFailedRotatingEncryption)
}
return
}
case err, ok := <-svc.keysetFileWatcher.Errors:
if !ok {
log.Fatal().Err(err).Msgf(errMessageTinkKeysetFileWatchFailed)
}
}
}
}

View file

@ -0,0 +1,146 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package encryption
import (
"fmt"
"github.com/rs/zerolog/log"
)
func (svc *tinkEncryptionService) enable() error {
err := svc.callbackOnEnable()
if err != nil {
return fmt.Errorf(errTemplateFailedEnablingEncryption, err)
}
err = svc.updateCiphertextSample()
if err != nil {
return fmt.Errorf(errTemplateFailedEnablingEncryption, err)
}
log.Warn().Msg(logMessageEncryptionEnabled)
return nil
}
func (svc *tinkEncryptionService) disable() error {
err := svc.callbackOnDisable()
if err != nil {
return fmt.Errorf(errTemplateFailedDisablingEncryption, err)
}
err = svc.deleteCiphertextSample()
if err != nil {
return fmt.Errorf(errTemplateFailedDisablingEncryption, err)
}
log.Warn().Msg(logMessageEncryptionDisabled)
return nil
}
func (svc *tinkEncryptionService) rotate() error {
newSvc := &tinkEncryptionService{
keysetFilePath: svc.keysetFilePath,
primaryKeyID: "",
encryption: nil,
store: svc.store,
keysetFileWatcher: nil,
clients: svc.clients,
}
err := newSvc.loadKeyset()
if err != nil {
return fmt.Errorf(errTemplateFailedRotatingEncryption, err)
}
err = newSvc.validateKeyset()
if err == errEncryptionKeyRotated {
err = newSvc.updateCiphertextSample()
}
if err != nil {
return fmt.Errorf(errTemplateFailedRotatingEncryption, err)
}
err = newSvc.callbackOnRotation()
if err != nil {
return fmt.Errorf(errTemplateFailedRotatingEncryption, err)
}
err = newSvc.initFileWatcher()
if err != nil {
return fmt.Errorf(errTemplateFailedRotatingEncryption, err)
}
return nil
}
func (svc *tinkEncryptionService) updateCiphertextSample() error {
ciphertext, err := svc.Encrypt(svc.primaryKeyID, keyIDAssociatedData)
if err != nil {
return fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
err = svc.store.ServerConfigSet(ciphertextSampleConfigKey, ciphertext)
if err != nil {
return fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
log.Info().Msg(logMessageEncryptionKeyRegistered)
return nil
}
func (svc *tinkEncryptionService) deleteCiphertextSample() error {
err := svc.store.ServerConfigDelete(ciphertextSampleConfigKey)
if err != nil {
err = fmt.Errorf(errTemplateFailedUpdatingServerConfig, err)
}
return err
}
func (svc *tinkEncryptionService) initClients() error {
for _, client := range svc.clients {
err := client.SetEncryptionService(svc)
if err != nil {
return err
}
}
log.Info().Msg(logMessageClientsInitialized)
return nil
}
func (svc *tinkEncryptionService) callbackOnEnable() error {
for _, client := range svc.clients {
err := client.EnableEncryption()
if err != nil {
return err
}
}
log.Info().Msg(logMessageClientsEnabled)
return nil
}
func (svc *tinkEncryptionService) callbackOnRotation() error {
for _, client := range svc.clients {
err := client.MigrateEncryption(svc)
if err != nil {
return err
}
}
log.Info().Msg(logMessageClientsRotated)
return nil
}
func (svc *tinkEncryptionService) callbackOnDisable() error {
for _, client := range svc.clients {
err := client.MigrateEncryption(&noEncryption{})
if err != nil {
return err
}
}
log.Info().Msg(logMessageClientsDecrypted)
return nil
}

View file

@ -0,0 +1,32 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package store
const (
errMessageTemplateFailedToEnable = "failed enabling secret store encryption: %w"
errMessageTemplateFailedToMigrate = "failed migrating secret store encryption: %w"
errMessageTemplateFailedToEncryptSecret = "failed to encrypt secret id=%d: %w"
errMessageTemplateFailedToDecryptSecret = "failed to decrypt secret id=%d: %w"
errMessageTemplateStorageError = "Storage error: could not update secret in DB"
errMessageTemplateFailedToRollbackSecretCreation = "failed creating secret: %w. Also failed deleting temporary secret record from store: %s"
errMessageInitSeveralTimes = "attempt to init encrypted storage more than once"
logMessageEnablingSecretsEncryption = "Encrypting all secrets in database"
logMessageEnablingSecretsEncryptionSuccess = "All secrets are encrypted"
logMessageMigratingSecretsEncryption = "Migrating encryption keys"
logMessageMigratingSecretsEncryptionSuccess = "Secrets encryption migrated successfully"
)

View file

@ -0,0 +1,165 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package store
import (
"fmt"
"github.com/woodpecker-ci/woodpecker/server/model"
)
func (wrapper *EncryptedSecretStore) SecretFind(repo *model.Repo, s string) (*model.Secret, error) {
result, err := wrapper.store.SecretFind(repo, s)
if err != nil {
return nil, err
}
err = wrapper.decrypt(result)
if err != nil {
return nil, err
}
return result, nil
}
func (wrapper *EncryptedSecretStore) SecretList(repo *model.Repo, b bool) ([]*model.Secret, error) {
results, err := wrapper.store.SecretList(repo, b)
if err != nil {
return nil, err
}
err = wrapper.decryptList(results)
if err != nil {
return nil, err
}
return results, nil
}
func (wrapper *EncryptedSecretStore) SecretCreate(secret *model.Secret) error {
newSecret := &model.Secret{}
err := wrapper.store.SecretCreate(newSecret)
if err != nil {
return err
}
secret.ID = newSecret.ID
err = wrapper.encrypt(secret)
if err != nil {
deleteErr := wrapper.store.SecretDelete(newSecret)
if deleteErr != nil {
return fmt.Errorf(errMessageTemplateFailedToRollbackSecretCreation, err, deleteErr.Error())
}
return err
}
err = wrapper.store.SecretUpdate(secret)
if err != nil {
deleteErr := wrapper.store.SecretDelete(newSecret)
if deleteErr != nil {
return fmt.Errorf(errMessageTemplateFailedToRollbackSecretCreation, err, deleteErr.Error())
}
return err
}
err = wrapper.decrypt(secret)
if err != nil {
return err
}
return nil
}
func (wrapper *EncryptedSecretStore) SecretUpdate(secret *model.Secret) error {
err := wrapper.encrypt(secret)
if err != nil {
return err
}
err = wrapper.store.SecretUpdate(secret)
if err != nil {
return err
}
err = wrapper.decrypt(secret)
if err != nil {
return err
}
return nil
}
func (wrapper *EncryptedSecretStore) SecretDelete(secret *model.Secret) error {
return wrapper.store.SecretDelete(secret)
}
func (wrapper *EncryptedSecretStore) OrgSecretFind(s, s2 string) (*model.Secret, error) {
result, err := wrapper.store.OrgSecretFind(s, s2)
if err != nil {
return nil, err
}
err = wrapper.decrypt(result)
if err != nil {
return nil, err
}
return result, nil
}
func (wrapper *EncryptedSecretStore) OrgSecretList(s string) ([]*model.Secret, error) {
results, err := wrapper.store.OrgSecretList(s)
if err != nil {
return nil, err
}
err = wrapper.decryptList(results)
if err != nil {
return nil, err
}
return results, nil
}
func (wrapper *EncryptedSecretStore) GlobalSecretFind(s string) (*model.Secret, error) {
result, err := wrapper.store.GlobalSecretFind(s)
if err != nil {
return nil, err
}
err = wrapper.decrypt(result)
if err != nil {
return nil, err
}
return result, nil
}
func (wrapper *EncryptedSecretStore) GlobalSecretList() ([]*model.Secret, error) {
results, err := wrapper.store.GlobalSecretList()
if err != nil {
return nil, err
}
err = wrapper.decryptList(results)
if err != nil {
return nil, err
}
return results, nil
}
func (wrapper *EncryptedSecretStore) SecretListAll() ([]*model.Secret, error) {
results, err := wrapper.store.SecretListAll()
if err != nil {
return nil, err
}
err = wrapper.decryptList(results)
if err != nil {
return nil, err
}
return results, nil
}

View file

@ -0,0 +1,123 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package store
import (
"errors"
"fmt"
"strconv"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server/model"
)
type EncryptedSecretStore struct {
store model.SecretStore
encryption model.EncryptionService
}
// ensure wrapper match interface
var _ model.SecretStore = new(EncryptedSecretStore)
func NewSecretStore(secretStore model.SecretStore) *EncryptedSecretStore {
wrapper := EncryptedSecretStore{secretStore, nil}
return &wrapper
}
func (wrapper *EncryptedSecretStore) SetEncryptionService(service model.EncryptionService) error {
if wrapper.encryption != nil {
return errors.New(errMessageInitSeveralTimes)
}
wrapper.encryption = service
return nil
}
func (wrapper *EncryptedSecretStore) EnableEncryption() error {
log.Warn().Msg(logMessageEnablingSecretsEncryption)
secrets, err := wrapper.store.SecretListAll()
if err != nil {
return fmt.Errorf(errMessageTemplateFailedToEnable, err)
}
for _, secret := range secrets {
if err := wrapper.encrypt(secret); err != nil {
return err
}
if err := wrapper._save(secret); err != nil {
return err
}
}
log.Warn().Msg(logMessageEnablingSecretsEncryptionSuccess)
return nil
}
func (wrapper *EncryptedSecretStore) MigrateEncryption(newEncryptionService model.EncryptionService) error {
log.Warn().Msg(logMessageMigratingSecretsEncryption)
secrets, err := wrapper.store.SecretListAll()
if err != nil {
return fmt.Errorf(errMessageTemplateFailedToMigrate, err)
}
if err := wrapper.decryptList(secrets); err != nil {
return err
}
wrapper.encryption = newEncryptionService
for _, secret := range secrets {
if err := wrapper.encrypt(secret); err != nil {
return err
}
if err := wrapper._save(secret); err != nil {
return err
}
}
log.Warn().Msg(logMessageMigratingSecretsEncryptionSuccess)
return nil
}
func (wrapper *EncryptedSecretStore) encrypt(secret *model.Secret) error {
encryptedValue, err := wrapper.encryption.Encrypt(secret.Value, strconv.Itoa(int(secret.ID)))
if err != nil {
return fmt.Errorf(errMessageTemplateFailedToEncryptSecret, secret.ID, err)
}
secret.Value = encryptedValue
return nil
}
func (wrapper *EncryptedSecretStore) decrypt(secret *model.Secret) error {
decryptedValue, err := wrapper.encryption.Decrypt(secret.Value, strconv.Itoa(int(secret.ID)))
if err != nil {
return fmt.Errorf(errMessageTemplateFailedToDecryptSecret, secret.ID, err)
}
secret.Value = decryptedValue
return nil
}
func (wrapper *EncryptedSecretStore) decryptList(secrets []*model.Secret) error {
for _, secret := range secrets {
err := wrapper.decrypt(secret)
if err != nil {
return fmt.Errorf(errMessageTemplateFailedToDecryptSecret, secret.ID, err)
}
}
return nil
}
func (wrapper *EncryptedSecretStore) _save(secret *model.Secret) error {
err := wrapper.store.SecretUpdate(secret)
if err != nil {
log.Err(err).Msg(errMessageTemplateStorageError)
return err
}
return nil
}

View file

@ -15,9 +15,9 @@
package datastore
import (
"github.com/woodpecker-ci/woodpecker/server/model"
"xorm.io/builder"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const orderSecretsBy = "secret_name"
@ -40,6 +40,11 @@ func (s storage) SecretList(repo *model.Repo, includeGlobalAndOrgSecrets bool) (
return secrets, s.engine.Where(cond).OrderBy(orderSecretsBy).Find(&secrets)
}
func (s storage) SecretListAll() ([]*model.Secret, error) {
var secrets []*model.Secret
return secrets, s.engine.Find(&secrets)
}
func (s storage) SecretCreate(secret *model.Secret) error {
// only Insert set auto created ID back to object
_, err := s.engine.Insert(secret)

View file

@ -78,6 +78,17 @@ func TestSecretList(t *testing.T) {
assert.Len(t, list, 2)
}
func TestSecretListAll(t *testing.T) {
store, closer := newTestStore(t, new(model.Secret))
defer closer()
createTestSecrets(t, store)
list, err := store.SecretListAll()
assert.NoError(t, err)
assert.Len(t, list, 4)
}
func TestSecretPipelineList(t *testing.T) {
store, closer := newTestStore(t, new(model.Secret))
defer closer()

View file

@ -17,8 +17,7 @@ func (s storage) ServerConfigGet(key string) (string, error) {
func (s storage) ServerConfigSet(key, value string) error {
config := &model.ServerConfig{
Key: key,
Value: value,
Key: key,
}
count, err := s.engine.Count(config)
@ -26,6 +25,8 @@ func (s storage) ServerConfigSet(key, value string) error {
return err
}
config.Value = value
if count == 0 {
_, err := s.engine.Insert(config)
return err
@ -34,3 +35,12 @@ func (s storage) ServerConfigSet(key, value string) error {
_, err = s.engine.Where("key = ?", config.Key).AllCols().Update(config)
return err
}
func (s storage) ServerConfigDelete(key string) error {
config := &model.ServerConfig{
Key: key,
}
_, err := s.engine.Delete(config)
return err
}

View file

@ -6,6 +6,7 @@ import (
io "io"
mock "github.com/stretchr/testify/mock"
model "github.com/woodpecker-ci/woodpecker/server/model"
)
@ -1348,6 +1349,29 @@ func (_m *Store) SecretList(_a0 *model.Repo, _a1 bool) ([]*model.Secret, error)
return r0, r1
}
// SecretList provides a mock function
func (_m *Store) SecretListAll() ([]*model.Secret, error) {
ret := _m.Called()
var r0 []*model.Secret
if rf, ok := ret.Get(0).(func() []*model.Secret); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.Secret)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SecretUpdate provides a mock function with given fields: _a0
func (_m *Store) SecretUpdate(_a0 *model.Secret) error {
ret := _m.Called(_a0)
@ -1397,6 +1421,20 @@ func (_m *Store) ServerConfigSet(_a0 string, _a1 string) error {
return r0
}
// ServerConfigDelete provides a mock function with given fields: _a0
func (_m *Store) ServerConfigDelete(_a0 string) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// StepChild provides a mock function with given fields: _a0, _a1, _a2
func (_m *Store) StepChild(_a0 *model.Pipeline, _a1 int, _a2 string) (*model.Step, error) {
ret := _m.Called(_a0, _a1, _a2)

View file

@ -122,6 +122,7 @@ type Store interface {
// Secrets
SecretFind(*model.Repo, string) (*model.Secret, error)
SecretList(*model.Repo, bool) ([]*model.Secret, error)
SecretListAll() ([]*model.Secret, error)
SecretCreate(*model.Secret) error
SecretUpdate(*model.Secret) error
SecretDelete(*model.Secret) error
@ -167,6 +168,7 @@ type Store interface {
// ServerConfig
ServerConfigGet(string) (string, error)
ServerConfigSet(string, string) error
ServerConfigDelete(string) error
// Cron
CronCreate(*model.Cron) error