Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-db/go/encryption.go
2497 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package db
6
7
import (
8
"bytes"
9
"crypto/aes"
10
"crypto/cipher"
11
"crypto/rand"
12
"encoding/base64"
13
"encoding/json"
14
"errors"
15
"fmt"
16
"os"
17
)
18
19
type Encryptor interface {
20
Encrypt(data []byte) (EncryptedData, error)
21
}
22
23
type Decryptor interface {
24
Decrypt(data EncryptedData) ([]byte, error)
25
}
26
27
type Cipher interface {
28
Encryptor
29
Decryptor
30
}
31
32
func NewAES256CBCCipher(secret string, metadata CipherMetadata) (*AES256CBC, error) {
33
block, err := aes.NewCipher([]byte(secret))
34
if err != nil {
35
return nil, fmt.Errorf("failed to initialize AES 256 CBC cipher block: %w", err)
36
}
37
38
return &AES256CBC{
39
block: block,
40
metadata: metadata,
41
}, nil
42
}
43
44
type AES256CBC struct {
45
block cipher.Block
46
metadata CipherMetadata
47
}
48
49
func (c *AES256CBC) Encrypt(data []byte) (EncryptedData, error) {
50
iv, err := GenerateInitializationVector(16)
51
if err != nil {
52
return EncryptedData{}, err
53
}
54
55
// In CBC mode, the plaintext has to be padded to align with the cipher's block size
56
plaintext := pad(data, c.block.BlockSize())
57
ciphertext := make([]byte, len(plaintext))
58
59
cbc := cipher.NewCBCEncrypter(c.block, iv)
60
cbc.CryptBlocks(ciphertext, plaintext)
61
62
encoded := base64.StdEncoding.EncodeToString(ciphertext)
63
return EncryptedData{
64
EncodedData: encoded,
65
Params: KeyParams{
66
InitializationVector: base64.StdEncoding.EncodeToString(iv),
67
},
68
Metadata: c.metadata,
69
}, nil
70
}
71
72
func (c *AES256CBC) Decrypt(data EncryptedData) ([]byte, error) {
73
if data.Metadata != c.metadata {
74
return nil, errors.New("cipher metadata does not match")
75
}
76
77
if data.Params.InitializationVector == "" {
78
return nil, errors.New("encrypted data does not contain an initialization vector")
79
}
80
81
ciphertext, err := base64.StdEncoding.DecodeString(data.EncodedData)
82
if err != nil {
83
return nil, fmt.Errorf("failed to decode ciphertext: %w", err)
84
}
85
86
plaintext := make([]byte, len(ciphertext))
87
88
iv, err := base64.StdEncoding.DecodeString(data.Params.InitializationVector)
89
if err != nil {
90
return nil, fmt.Errorf("failed to decode initialize vector from base64: %w", err)
91
}
92
93
cbc := cipher.NewCBCDecrypter(c.block, iv)
94
cbc.CryptBlocks(plaintext, ciphertext)
95
96
// In CBC mode, the plaintext was padded to align with the cipher's block size, we need to trim
97
trimmed := trim(plaintext)
98
return trimmed, nil
99
}
100
101
type KeyParams struct {
102
InitializationVector string `json:"iv"`
103
}
104
105
type CipherMetadata struct {
106
Name string `json:"name"`
107
Version int `json:"version"`
108
}
109
110
// EncryptedData represents the data stored in an encrypted entry
111
// The JSON fields must match the existing implementation on server, in
112
// components/gitpod-protocol/src/encryption/encryption-engine.ts
113
type EncryptedData struct {
114
// EncodedData is base64 encoded
115
EncodedData string `json:"data"`
116
// Params contain additional data needed for encryption/decryption
117
Params KeyParams `json:"keyParams"`
118
// Metadata contains metadata about the cipher used to encrypt the data
119
Metadata CipherMetadata `json:"keyMetadata"`
120
}
121
122
func GenerateInitializationVector(size int) ([]byte, error) {
123
buf := make([]byte, size)
124
_, err := rand.Read(buf)
125
if err != nil {
126
return nil, fmt.Errorf("failed to generate initialization vector: %w", err)
127
}
128
129
return buf, nil
130
}
131
132
func pad(ciphertext []byte, blockSize int) []byte {
133
padding := blockSize - len(ciphertext)%blockSize
134
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
135
return append(ciphertext, padtext...)
136
}
137
func trim(encrypt []byte) []byte {
138
padding := encrypt[len(encrypt)-1]
139
return encrypt[:len(encrypt)-int(padding)]
140
}
141
142
type CipherConfig struct {
143
Name string `json:"name"`
144
Version int `json:"version"`
145
Primary bool `json:"primary"`
146
// Material is the secret key, it is base64 encoded
147
Material string `json:"material"`
148
}
149
150
func NewCipherSetFromKeysInFile(pathToKeys string) (*CipherSet, error) {
151
b, err := os.ReadFile(pathToKeys)
152
if err != nil {
153
return nil, fmt.Errorf("failed to read file: %w", err)
154
}
155
156
var cfg []CipherConfig
157
err = json.Unmarshal(b, &cfg)
158
if err != nil {
159
return nil, fmt.Errorf("failed to unmarhsal cipher config: %w", err)
160
}
161
162
return NewCipherSet(cfg)
163
}
164
165
func NewCipherSet(configs []CipherConfig) (*CipherSet, error) {
166
if len(configs) == 0 {
167
return nil, errors.New("no cipher config specified, at least one cipher config required")
168
}
169
170
primaries := findPrimaryConfigs(configs)
171
if len(primaries) == 0 {
172
return nil, errors.New("no primaries cipher config specified, exactly one primaries config is required")
173
}
174
if len(primaries) >= 2 {
175
return nil, errors.New("more than one primaries cipher config specified, exactly one is requires")
176
}
177
178
primary := primaries[0]
179
primaryCipher, err := cipherConfigToAES256CBC(primary)
180
if err != nil {
181
return nil, fmt.Errorf("failed to construct primary cipher: %w", err)
182
}
183
184
var ciphers []*AES256CBC
185
for _, c := range configs {
186
ciph, err := cipherConfigToAES256CBC(c)
187
if err != nil {
188
return nil, fmt.Errorf("failed to construct non-primary cipher for config named %s: %w", c.Name, err)
189
}
190
191
ciphers = append(ciphers, ciph)
192
}
193
194
return &CipherSet{
195
ciphers: ciphers,
196
primary: primaryCipher,
197
}, nil
198
}
199
200
type CipherSet struct {
201
ciphers []*AES256CBC
202
primary *AES256CBC
203
}
204
205
func (cs *CipherSet) Encrypt(data []byte) (EncryptedData, error) {
206
// We only encrypt using the primary cipher
207
return cs.primary.Encrypt(data)
208
}
209
210
func (cs *CipherSet) Decrypt(data EncryptedData) ([]byte, error) {
211
// We attempt to decrypt using all ciphers. based on matching metadata. This ensures that ciphers can be rotated over time.
212
metadata := data.Metadata
213
for _, c := range cs.ciphers {
214
if c.metadata == metadata {
215
return c.Decrypt(data)
216
}
217
}
218
219
return nil, fmt.Errorf("no cipher matching metadata (%s, %d) configured", metadata.Name, metadata.Version)
220
}
221
222
func findPrimaryConfigs(cfgs []CipherConfig) []CipherConfig {
223
var primary []CipherConfig
224
for _, c := range cfgs {
225
if c.Primary {
226
primary = append(primary, c)
227
}
228
}
229
return primary
230
}
231
232
func cipherConfigToAES256CBC(cfg CipherConfig) (*AES256CBC, error) {
233
keyDecoded, err := base64.StdEncoding.DecodeString(cfg.Material)
234
if err != nil {
235
return nil, fmt.Errorf("failed to decode ciph config material from base64: %w", err)
236
}
237
ciph, err := NewAES256CBCCipher(string(keyDecoded), CipherMetadata{
238
Name: cfg.Name,
239
Version: cfg.Version,
240
})
241
if err != nil {
242
return nil, fmt.Errorf("failed to construct AES 256 CBC ciph: %w", err)
243
}
244
return ciph, nil
245
}
246
247