mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-01 20:22:21 +00:00
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:
parent
f71142d162
commit
6516a28cdd
31 changed files with 1633 additions and 8 deletions
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
64
docs/docs/30-administration/40-encryption.md
Normal file
64
docs/docs/30-administration/40-encryption.md
Normal 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
5
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
41
server/model/encryption.go
Normal file
41
server/model/encryption.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
|
|
67
server/plugins/encryption/aes.go
Normal file
67
server/plugins/encryption/aes.go
Normal 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()
|
||||
}
|
66
server/plugins/encryption/aes_builder.go
Normal file
66
server/plugins/encryption/aes_builder.go
Normal 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
|
||||
}
|
83
server/plugins/encryption/aes_encryption.go
Normal file
83
server/plugins/encryption/aes_encryption.go
Normal 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
|
||||
}
|
101
server/plugins/encryption/aes_state.go
Normal file
101
server/plugins/encryption/aes_state.go
Normal 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
|
||||
}
|
50
server/plugins/encryption/aes_test.go
Normal file
50
server/plugins/encryption/aes_test.go
Normal 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)
|
||||
}
|
99
server/plugins/encryption/constants.go
Normal file
99
server/plugins/encryption/constants.go
Normal 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"
|
||||
)
|
72
server/plugins/encryption/encryption.go
Normal file
72
server/plugins/encryption/encryption.go
Normal 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
|
||||
}
|
73
server/plugins/encryption/encryption_builder.go
Normal file
73
server/plugins/encryption/encryption_builder.go
Normal 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)
|
||||
}
|
||||
}
|
51
server/plugins/encryption/no_encryption.go
Normal file
51
server/plugins/encryption/no_encryption.go
Normal 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
|
||||
}
|
62
server/plugins/encryption/tink.go
Normal file
62
server/plugins/encryption/tink.go
Normal 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()
|
||||
}
|
77
server/plugins/encryption/tink_builder.go
Normal file
77
server/plugins/encryption/tink_builder.go
Normal 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
|
||||
}
|
74
server/plugins/encryption/tink_keyset.go
Normal file
74
server/plugins/encryption/tink_keyset.go
Normal 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
|
||||
}
|
61
server/plugins/encryption/tink_keyset_watcher.go
Normal file
61
server/plugins/encryption/tink_keyset_watcher.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
server/plugins/encryption/tink_state.go
Normal file
146
server/plugins/encryption/tink_state.go
Normal 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
|
||||
}
|
32
server/plugins/encryption/wrapper/store/constants.go
Normal file
32
server/plugins/encryption/wrapper/store/constants.go
Normal 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"
|
||||
)
|
165
server/plugins/encryption/wrapper/store/secret_store.go
Normal file
165
server/plugins/encryption/wrapper/store/secret_store.go
Normal 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
|
||||
}
|
123
server/plugins/encryption/wrapper/store/secret_store_wrapper.go
Normal file
123
server/plugins/encryption/wrapper/store/secret_store_wrapper.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -18,7 +18,6 @@ func (s storage) ServerConfigGet(key string) (string, error) {
|
|||
func (s storage) ServerConfigSet(key, value string) error {
|
||||
config := &model.ServerConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue