mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-05-04 23:38:44 +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
|
@ -525,4 +525,23 @@ var flags = []cli.Flag{
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
// TODO(485) temporary workaround to not hit api rate limits
|
// 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/logging"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/plugins/config"
|
"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/pubsub"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/router"
|
"github.com/woodpecker-ci/woodpecker/server/router"
|
||||||
"github.com/woodpecker-ci/woodpecker/server/router/middleware"
|
"github.com/woodpecker-ci/woodpecker/server/router/middleware"
|
||||||
|
@ -260,6 +262,13 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
|
||||||
// forge
|
// forge
|
||||||
server.Config.Services.Forge = f
|
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
|
// services
|
||||||
server.Config.Services.Queue = setupQueue(c, v)
|
server.Config.Services.Queue = setupQueue(c, v)
|
||||||
server.Config.Services.Logs = logging.New()
|
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")
|
log.Error().Err(err).Msg("could not create pubsub service")
|
||||||
}
|
}
|
||||||
server.Config.Services.Registries = setupRegistryService(c, v)
|
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.Environ = setupEnvironService(c, v)
|
||||||
server.Config.Services.Membership = setupMembershipService(c, f)
|
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)
|
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)
|
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
|
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`
|
### `WOODPECKER_PROMETHEUS_AUTH_TOKEN`
|
||||||
> Default: empty
|
> 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/docker/go-units v0.5.0
|
||||||
github.com/drone/envsubst v1.0.3
|
github.com/drone/envsubst v1.0.3
|
||||||
github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf
|
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/gin-gonic/gin v1.8.1
|
||||||
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51
|
github.com/go-ap/httpsig v0.0.0-20210714162115-62a09257db51
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||||
github.com/google/go-github/v39 v39.2.0
|
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/gorilla/securecookie v1.1.1
|
||||||
github.com/joho/godotenv v1.4.0
|
github.com/joho/godotenv v1.4.0
|
||||||
github.com/lafriks/ttlcache/v3 v3.2.0
|
github.com/lafriks/ttlcache/v3 v3.2.0
|
||||||
|
@ -39,6 +41,7 @@ require (
|
||||||
github.com/urfave/cli/v2 v2.20.2
|
github.com/urfave/cli/v2 v2.20.2
|
||||||
github.com/xanzy/go-gitlab v0.73.1
|
github.com/xanzy/go-gitlab v0.73.1
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
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/exp v0.0.0-20221031165847-c99f073a8326
|
||||||
golang.org/x/net v0.4.0
|
golang.org/x/net v0.4.0
|
||||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783
|
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/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||||
github.com/fatih/color v1.13.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/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // 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/atomic v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.8.0 // indirect
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
go.uber.org/zap v1.23.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/mod v0.6.0 // indirect
|
||||||
golang.org/x/sys v0.3.0 // indirect
|
golang.org/x/sys v0.3.0 // indirect
|
||||||
golang.org/x/term 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-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/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/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.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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
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)
|
OrgSecretList(string) ([]*Secret, error)
|
||||||
GlobalSecretFind(string) (*Secret, error)
|
GlobalSecretFind(string) (*Secret, error)
|
||||||
GlobalSecretList() ([]*Secret, error)
|
GlobalSecretList() ([]*Secret, error)
|
||||||
|
SecretListAll() ([]*Secret, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secret represents a secret variable, such as a password or token.
|
// 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
|
package datastore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/woodpecker-ci/woodpecker/server/model"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
|
||||||
|
"github.com/woodpecker-ci/woodpecker/server/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const orderSecretsBy = "secret_name"
|
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)
|
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 {
|
func (s storage) SecretCreate(secret *model.Secret) error {
|
||||||
// only Insert set auto created ID back to object
|
// only Insert set auto created ID back to object
|
||||||
_, err := s.engine.Insert(secret)
|
_, err := s.engine.Insert(secret)
|
||||||
|
|
|
@ -78,6 +78,17 @@ func TestSecretList(t *testing.T) {
|
||||||
assert.Len(t, list, 2)
|
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) {
|
func TestSecretPipelineList(t *testing.T) {
|
||||||
store, closer := newTestStore(t, new(model.Secret))
|
store, closer := newTestStore(t, new(model.Secret))
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
|
@ -17,8 +17,7 @@ func (s storage) ServerConfigGet(key string) (string, error) {
|
||||||
|
|
||||||
func (s storage) ServerConfigSet(key, value string) error {
|
func (s storage) ServerConfigSet(key, value string) error {
|
||||||
config := &model.ServerConfig{
|
config := &model.ServerConfig{
|
||||||
Key: key,
|
Key: key,
|
||||||
Value: value,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := s.engine.Count(config)
|
count, err := s.engine.Count(config)
|
||||||
|
@ -26,6 +25,8 @@ func (s storage) ServerConfigSet(key, value string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.Value = value
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
_, err := s.engine.Insert(config)
|
_, err := s.engine.Insert(config)
|
||||||
return err
|
return err
|
||||||
|
@ -34,3 +35,12 @@ func (s storage) ServerConfigSet(key, value string) error {
|
||||||
_, err = s.engine.Where("key = ?", config.Key).AllCols().Update(config)
|
_, err = s.engine.Where("key = ?", config.Key).AllCols().Update(config)
|
||||||
return err
|
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"
|
io "io"
|
||||||
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
model "github.com/woodpecker-ci/woodpecker/server/model"
|
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
|
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
|
// SecretUpdate provides a mock function with given fields: _a0
|
||||||
func (_m *Store) SecretUpdate(_a0 *model.Secret) error {
|
func (_m *Store) SecretUpdate(_a0 *model.Secret) error {
|
||||||
ret := _m.Called(_a0)
|
ret := _m.Called(_a0)
|
||||||
|
@ -1397,6 +1421,20 @@ func (_m *Store) ServerConfigSet(_a0 string, _a1 string) error {
|
||||||
return r0
|
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
|
// 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) {
|
func (_m *Store) StepChild(_a0 *model.Pipeline, _a1 int, _a2 string) (*model.Step, error) {
|
||||||
ret := _m.Called(_a0, _a1, _a2)
|
ret := _m.Called(_a0, _a1, _a2)
|
||||||
|
|
|
@ -122,6 +122,7 @@ type Store interface {
|
||||||
// Secrets
|
// Secrets
|
||||||
SecretFind(*model.Repo, string) (*model.Secret, error)
|
SecretFind(*model.Repo, string) (*model.Secret, error)
|
||||||
SecretList(*model.Repo, bool) ([]*model.Secret, error)
|
SecretList(*model.Repo, bool) ([]*model.Secret, error)
|
||||||
|
SecretListAll() ([]*model.Secret, error)
|
||||||
SecretCreate(*model.Secret) error
|
SecretCreate(*model.Secret) error
|
||||||
SecretUpdate(*model.Secret) error
|
SecretUpdate(*model.Secret) error
|
||||||
SecretDelete(*model.Secret) error
|
SecretDelete(*model.Secret) error
|
||||||
|
@ -167,6 +168,7 @@ type Store interface {
|
||||||
// ServerConfig
|
// ServerConfig
|
||||||
ServerConfigGet(string) (string, error)
|
ServerConfigGet(string) (string, error)
|
||||||
ServerConfigSet(string, string) error
|
ServerConfigSet(string, string) error
|
||||||
|
ServerConfigDelete(string) error
|
||||||
|
|
||||||
// Cron
|
// Cron
|
||||||
CronCreate(*model.Cron) error
|
CronCreate(*model.Cron) error
|
||||||
|
|
Loading…
Reference in a new issue