12
12
// - database.DBCryptSentinelValue
13
13
//
14
14
// Encrypted fields are stored in the following format:
15
- // "dbcrypt-<first 7 characters of cipher's SHA256 digest>-<base64-encoded encrypted value>"
15
+ // "dbcrypt-${b64encode( <first 7 digits of cipher's SHA256 digest>-<encrypted value>)} "
16
16
//
17
17
// The first 7 characters of the cipher's SHA256 digest are used to identify the cipher
18
18
// used to encrypt the value.
19
19
//
20
- // Two ciphers can be provided to support key rotation. The primary cipher is used to encrypt
21
- // and decrypt all values. We only use the secondary cipher to decrypt values if decryption
22
- // with the primary cipher fails .
20
+ // Multiple ciphers can be provided to support key rotation. The primary cipher is used
21
+ // to encrypt and decrypt all data. Secondary ciphers are only used for decryption.
22
+ // We currently only use a single secondary cipher .
23
23
package dbcrypt
24
24
25
25
import (
@@ -28,16 +28,12 @@ import (
28
28
"encoding/base64"
29
29
"errors"
30
30
"strings"
31
- "sync/atomic"
32
-
33
- "github.com/google/uuid"
34
- "github.com/hashicorp/go-multierror"
35
- "golang.org/x/xerrors"
36
-
37
- "cdr.dev/slog"
38
31
39
32
"github.com/coder/coder/v2/coderd/database"
40
33
"github.com/coder/coder/v2/coderd/database/dbauthz"
34
+
35
+ "github.com/google/uuid"
36
+ "golang.org/x/xerrors"
41
37
)
42
38
43
39
// MagicPrefix is prepended to all encrypted values in the database.
@@ -48,10 +44,6 @@ import (
48
44
// encrypted value.
49
45
const MagicPrefix = "dbcrypt-"
50
46
51
- // MagicPrefixLength is the length of the entire prefix used to identify
52
- // encrypted values.
53
- const MagicPrefixLength = len (MagicPrefix ) + 8
54
-
55
47
// sentinelValue is the value that is stored in the database to indicate
56
48
// whether encryption is enabled. If not enabled, the value either not
57
49
// present, or is the raw string "coder".
@@ -79,31 +71,14 @@ func (*DecryptFailedError) Unwrap() error {
79
71
return sql .ErrNoRows
80
72
}
81
73
82
- func IsDecryptFailedError (err error ) bool {
83
- var e * DecryptFailedError
84
- return errors .As (err , & e )
85
- }
86
-
87
- type Options struct {
88
- // PrimaryCipher is an optional cipher that is used
89
- // to encrypt/decrypt user link and git auth link tokens. If this is nil,
90
- // then no encryption/decryption will be performed.
91
- PrimaryCipher * atomic.Pointer [Cipher ]
92
- // SecondaryCipher is an optional cipher that is only used
93
- // to decrypt user link and git auth link tokens.
94
- // This should only be used when rotating the primary cipher.
95
- SecondaryCipher * atomic.Pointer [Cipher ]
96
- Logger slog.Logger
97
- }
98
-
99
74
// New creates a database.Store wrapper that encrypts/decrypts values
100
75
// stored at rest in the database.
101
- func New (ctx context.Context , db database.Store , options * Options ) (database.Store , error ) {
102
- if options . PrimaryCipher . Load () == nil {
103
- return nil , xerrors .Errorf ("at least one cipher is required " )
76
+ func New (ctx context.Context , db database.Store , cs * Ciphers ) (database.Store , error ) {
77
+ if cs == nil {
78
+ return nil , xerrors .Errorf ("no ciphers configured " )
104
79
}
105
80
dbc := & dbCrypt {
106
- Options : options ,
81
+ ciphers : cs ,
107
82
Store : db ,
108
83
}
109
84
if err := ensureEncrypted (dbauthz .AsSystemRestricted (ctx ), dbc ); err != nil {
@@ -113,14 +88,14 @@ func New(ctx context.Context, db database.Store, options *Options) (database.Sto
113
88
}
114
89
115
90
type dbCrypt struct {
116
- * Options
91
+ ciphers * Ciphers
117
92
database.Store
118
93
}
119
94
120
95
func (db * dbCrypt ) InTx (function func (database.Store ) error , txOpts * sql.TxOptions ) error {
121
96
return db .Store .InTx (func (s database.Store ) error {
122
97
return function (& dbCrypt {
123
- Options : db .Options ,
98
+ ciphers : db .ciphers ,
124
99
Store : s ,
125
100
})
126
101
}, txOpts )
@@ -225,83 +200,52 @@ func (db *dbCrypt) SetDBCryptSentinelValue(ctx context.Context, value string) er
225
200
}
226
201
227
202
func (db * dbCrypt ) encryptFields (fields ... * string ) error {
228
- // Encryption ALWAYS happens with the primary cipher.
229
- cipherPtr := db .PrimaryCipher .Load ()
230
203
// If no cipher is loaded, then we can't encrypt anything!
231
- if cipherPtr == nil {
204
+ if db . ciphers == nil {
232
205
return ErrNotEnabled
233
206
}
234
- cipher := * cipherPtr
207
+
235
208
for _ , field := range fields {
236
209
if field == nil {
237
210
continue
238
211
}
239
212
240
- encrypted , err := cipher .Encrypt ([]byte (* field ))
213
+ encrypted , err := db . ciphers .Encrypt ([]byte (* field ))
241
214
if err != nil {
242
215
return err
243
216
}
244
217
// Base64 is used to support UTF-8 encoding in PostgreSQL.
245
- * field = MagicPrefix + cipher . HexDigest ()[: 7 ] + "-" + b64encode (encrypted )
218
+ * field = MagicPrefix + b64encode (encrypted )
246
219
}
247
220
return nil
248
221
}
249
222
250
223
// decryptFields decrypts the given fields in place.
251
224
// If the value fails to decrypt, sql.ErrNoRows will be returned.
252
225
func (db * dbCrypt ) decryptFields (fields ... * string ) error {
253
- var merr * multierror.Error
254
-
255
- // We try to decrypt with both the primary and secondary cipher.
256
- primaryCipherPtr := db .PrimaryCipher .Load ()
257
- if err := decryptWithCipher (primaryCipherPtr , fields ... ); err == nil {
258
- return nil
259
- } else {
260
- merr = multierror .Append (merr , err )
261
- }
262
- secondaryCipherPtr := db .SecondaryCipher .Load ()
263
- if err := decryptWithCipher (secondaryCipherPtr , fields ... ); err == nil {
264
- return nil
265
- } else {
266
- merr = multierror .Append (merr , err )
267
- }
268
- return merr
269
- }
270
-
271
- func decryptWithCipher (cipherPtr * Cipher , fields ... * string ) error {
272
- // If no cipher is loaded, then we can't decrypt anything!
273
- if cipherPtr == nil {
226
+ if db .ciphers == nil {
274
227
return ErrNotEnabled
275
228
}
276
229
277
- cipher := * cipherPtr
278
230
for _ , field := range fields {
279
231
if field == nil {
280
232
continue
281
233
}
282
234
283
- if len (* field ) < 16 || ! strings .HasPrefix (* field , MagicPrefix ) {
235
+ if len (* field ) < 8 || ! strings .HasPrefix (* field , MagicPrefix ) {
284
236
// We do not force decryption of unencrypted rows. This could be damaging
285
237
// to the deployment, and admins can always manually purge data.
286
238
continue
287
239
}
288
240
289
- // The first 7 characters of the digest are used to identify the cipher.
290
- // If the cipher changes, we should complain loudly.
291
- encPrefix := cipher .HexDigest ()[:7 ]
292
- if ! strings .HasPrefix ((* field )[8 :15 ], encPrefix ) {
293
- return & DecryptFailedError {
294
- Inner : xerrors .Errorf ("cipher mismatch: expected %q, got %q" , encPrefix , (* field )[8 :15 ]),
295
- }
296
- }
297
- data , err := b64decode ((* field )[16 :])
241
+ data , err := b64decode ((* field )[8 :])
298
242
if err != nil {
299
243
// If it's not base64 with the prefix, we should complain loudly.
300
244
return & DecryptFailedError {
301
245
Inner : xerrors .Errorf ("malformed encrypted field %q: %w" , * field , err ),
302
246
}
303
247
}
304
- decrypted , err := cipher .Decrypt (data )
248
+ decrypted , err := db . ciphers .Decrypt (data )
305
249
if err != nil {
306
250
// If the encryption key changed, return our special error that unwraps to sql.ErrNoRows.
307
251
return & DecryptFailedError {Inner : err }
0 commit comments