diff --git a/components/gitpod-db/go/dbtest/encryption.go b/components/gitpod-db/go/dbtest/encryption.go new file mode 100644 index 00000000000000..9caaba61854325 --- /dev/null +++ b/components/gitpod-db/go/dbtest/encryption.go @@ -0,0 +1,28 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package dbtest + +import ( + "encoding/base64" + db "github.com/gitpod-io/gitpod/components/gitpod-db/go" + "github.com/stretchr/testify/require" + "testing" +) + +func GetTestCipher(t *testing.T) (*db.AES256CBC, db.CipherMetadata) { + t.Helper() + + // This is a test key also used in server tests - see components/gitpod-protocol/src/encryption/encryption-engine.spec.ts + key, err := base64.StdEncoding.DecodeString("ZMaTPrF7s9gkLbY45zP59O0LTpLvDd/cgqPE9Ptghh8=") + require.NoError(t, err) + + metadata := db.CipherMetadata{ + Name: "default", + Version: 1, + } + cipher, err := db.NewAES256CBCCipher(string(key), metadata) + require.NoError(t, err) + return cipher, metadata +} diff --git a/components/gitpod-db/go/dbtest/oidc_client_config.go b/components/gitpod-db/go/dbtest/oidc_client_config.go index 2f131f0a58365b..bdfd3711e66ff9 100644 --- a/components/gitpod-db/go/dbtest/oidc_client_config.go +++ b/components/gitpod-db/go/dbtest/oidc_client_config.go @@ -17,11 +17,15 @@ import ( func NewOIDCClientConfig(t *testing.T, record db.OIDCClientConfig) db.OIDCClientConfig { t.Helper() + cipher, _ := GetTestCipher(t) + encrypted, err := db.EncryptJSON(cipher, db.OIDCSpec{}) + require.NoError(t, err) + now := time.Now().UTC().Truncate(time.Millisecond) result := db.OIDCClientConfig{ ID: uuid.New(), Issuer: "issuer", - Data: []byte("{}"), + Data: encrypted, LastModified: now, } diff --git a/components/gitpod-db/go/encryption.go b/components/gitpod-db/go/encryption.go index 539c0028db2a97..b3829945329893 100644 --- a/components/gitpod-db/go/encryption.go +++ b/components/gitpod-db/go/encryption.go @@ -14,11 +14,19 @@ import ( "fmt" ) -type Cipher interface { +type Encryptor interface { Encrypt(data []byte) (EncryptedData, error) +} + +type Decryptor interface { Decrypt(data EncryptedData) ([]byte, error) } +type Cipher interface { + Encryptor + Decryptor +} + func NewAES256CBCCipher(secret string, metadata CipherMetadata) (*AES256CBC, error) { block, err := aes.NewCipher([]byte(secret)) if err != nil { @@ -37,7 +45,7 @@ type AES256CBC struct { } func (c *AES256CBC) Encrypt(data []byte) (EncryptedData, error) { - iv, err := generateInitializationVector(16) + iv, err := GenerateInitializationVector(16) if err != nil { return EncryptedData{}, err } @@ -109,7 +117,7 @@ type EncryptedData struct { Metadata CipherMetadata `json:"keyMetadata"` } -func generateInitializationVector(size int) ([]byte, error) { +func GenerateInitializationVector(size int) ([]byte, error) { buf := make([]byte, size) _, err := rand.Read(buf) if err != nil { diff --git a/components/gitpod-db/go/encryption_test.go b/components/gitpod-db/go/encryption_test.go index 390e7e11ffc428..a661a1a56c92af 100644 --- a/components/gitpod-db/go/encryption_test.go +++ b/components/gitpod-db/go/encryption_test.go @@ -2,25 +2,26 @@ // Licensed under the GNU Affero General Public License (AGPL). // See License.AGPL.txt in the project root for license information. -package db +package db_test import ( "encoding/base64" "fmt" + db "github.com/gitpod-io/gitpod/components/gitpod-db/go" + "github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest" "github.com/stretchr/testify/require" "testing" ) func TestAES256CBCCipher_Encrypt_Decrypt(t *testing.T) { - secret, err := generateInitializationVector(32) - require.NoError(t, err) + secret := "testtesttesttesttesttesttesttest" - metadata := CipherMetadata{ + metadata := db.CipherMetadata{ Name: "general", Version: 1, } - cipher, err := NewAES256CBCCipher(string(secret), metadata) + cipher, err := db.NewAES256CBCCipher(secret, metadata) require.NoError(t, err) data := []byte(`{ "foo": "bar", "another": "one" }`) @@ -45,26 +46,15 @@ func TestAES256CBCCipher_Encrypt_Decrypt(t *testing.T) { } func TestAES256CBCCipher_EncryptedByServer(t *testing.T) { - // This is a test key also used in server tests - see components/gitpod-protocol/src/encryption/encryption-engine.spec.ts - key, err := base64.StdEncoding.DecodeString("ZMaTPrF7s9gkLbY45zP59O0LTpLvDd/cgqPE9Ptghh8=") - require.NoError(t, err) - - metadata := CipherMetadata{ - Name: "general", - Version: 1, - } - encrypted := EncryptedData{ - + cipher, metadata := dbtest.GetTestCipher(t) + encrypted := db.EncryptedData{ EncodedData: "YpgOY8ZNV64oG1DXiuCUXKy0thVySbN7uXTQxtC2j2A=", - Params: KeyParams{ + Params: db.KeyParams{ InitializationVector: "vpTOAFN5v4kOPsAHBKk+eg==", }, Metadata: metadata, } - cipher, err := NewAES256CBCCipher(string(key), metadata) - require.NoError(t, err) - decrypted, err := cipher.Decrypt(encrypted) fmt.Println(err) require.NoError(t, err) diff --git a/components/gitpod-db/go/json.go b/components/gitpod-db/go/json.go new file mode 100644 index 00000000000000..62d28d9ca6eabd --- /dev/null +++ b/components/gitpod-db/go/json.go @@ -0,0 +1,66 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package db + +import ( + "encoding/json" + "fmt" + "gorm.io/datatypes" +) + +type EncryptedJSON[T any] datatypes.JSON + +func (j *EncryptedJSON[T]) EncryptedData() (EncryptedData, error) { + var data EncryptedData + err := json.Unmarshal(*j, &data) + if err != nil { + return EncryptedData{}, fmt.Errorf("failed to unmarshal encrypted json: %w", err) + } + + return data, nil +} + +func (j *EncryptedJSON[T]) Decrypt(decryptor Decryptor) (T, error) { + var out T + data, err := j.EncryptedData() + if err != nil { + return out, fmt.Errorf("failed to obtain encrypted data: %w", err) + } + + b, err := decryptor.Decrypt(data) + if err != nil { + return out, fmt.Errorf("failed to decrypt encrypted json: %w", err) + } + + err = json.Unmarshal(b, &out) + if err != nil { + return out, fmt.Errorf("failed to unmarshal encrypted json: %w", err) + } + + return out, nil +} + +func EncryptJSON[T any](encryptor Encryptor, data T) (EncryptedJSON[T], error) { + b, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal data into json: %w", err) + } + + encrypted, err := encryptor.Encrypt(b) + if err != nil { + return nil, fmt.Errorf("failed to encrypt json: %w", err) + } + + return NewEncryptedJSON[T](encrypted) +} + +func NewEncryptedJSON[T any](data EncryptedData) (EncryptedJSON[T], error) { + b, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to serialize encrypted data into json: %w", err) + } + + return b, nil +} diff --git a/components/gitpod-db/go/json_test.go b/components/gitpod-db/go/json_test.go new file mode 100644 index 00000000000000..d6ed1423283a49 --- /dev/null +++ b/components/gitpod-db/go/json_test.go @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package db_test + +import ( + db "github.com/gitpod-io/gitpod/components/gitpod-db/go" + "github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest" + "github.com/stretchr/testify/require" + "testing" +) + +func TestEncryptJSON_DecryptJSON(t *testing.T) { + cipher, _ := dbtest.GetTestCipher(t) + + type Data struct { + First string + Second int + } + + data := Data{ + First: "first", + Second: 2, + } + + encrypted, err := db.EncryptJSON(cipher, data) + require.NoError(t, err) + + decrypted, err := encrypted.Decrypt(cipher) + require.NoError(t, err) + + require.Equal(t, data, decrypted) +} diff --git a/components/gitpod-db/go/oidc_client_config.go b/components/gitpod-db/go/oidc_client_config.go index 7d1696fc7af0c6..b8ebd72ce3e2c1 100644 --- a/components/gitpod-db/go/oidc_client_config.go +++ b/components/gitpod-db/go/oidc_client_config.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "github.com/google/uuid" - "gorm.io/datatypes" "gorm.io/gorm" "time" ) @@ -19,7 +18,7 @@ type OIDCClientConfig struct { Issuer string `gorm:"column:issuer;type:char;size:255;" json:"issuer"` - Data datatypes.JSON `gorm:"column:data;type:text;size:65535" json:"data"` + Data EncryptedJSON[OIDCSpec] `gorm:"column:data;type:text;size:65535" json:"data"` LastModified time.Time `gorm:"column:_lastModified;type:timestamp;default:CURRENT_TIMESTAMP(6);" json:"_lastModified"` // deleted is reserved for use by db-sync. @@ -30,6 +29,9 @@ func (c *OIDCClientConfig) TableName() string { return "d_b_oidc_client_config" } +type OIDCSpec struct { +} + func CreateOIDCCLientConfig(ctx context.Context, conn *gorm.DB, cfg OIDCClientConfig) (OIDCClientConfig, error) { if cfg.ID == uuid.Nil { return OIDCClientConfig{}, errors.New("OIDC Client Config ID must be set") diff --git a/components/gitpod-db/go/types.go b/components/gitpod-db/go/time.go similarity index 100% rename from components/gitpod-db/go/types.go rename to components/gitpod-db/go/time.go diff --git a/components/gitpod-db/go/types_test.go b/components/gitpod-db/go/time_test.go similarity index 100% rename from components/gitpod-db/go/types_test.go rename to components/gitpod-db/go/time_test.go