1
+ // Package dbcrypt provides a database.Store wrapper that encrypts/decrypts
2
+ // values stored at rest in the database.
3
+ //
4
+ // Encryption is done using a Cipher. The Cipher is stored in an atomic pointer
5
+ // so that it can be rotated as required.
6
+ //
7
+ // The Cipher is currently used to encrypt/decrypt the following fields:
8
+ // - database.UserLink.OAuthAccessToken
9
+ // - database.UserLink.OAuthRefreshToken
10
+ // - database.GitAuthLink.OAuthAccessToken
11
+ // - database.GitAuthLink.OAuthRefreshToken
12
+ // - database.DBCryptSentinelValue
13
+ //
14
+ // Encrypted fields are stored in the following format:
15
+ // "dbcrypt-<first 7 characters of cipher's SHA256 digest>-<base64-encoded encrypted value>"
16
+ //
17
+ // The first 7 characters of the cipher's SHA256 digest are used to identify the cipher
18
+ // used to encrypt the value.
1
19
package dbcrypt
2
20
3
21
import (
@@ -19,14 +37,46 @@ import (
19
37
// MagicPrefix is prepended to all encrypted values in the database.
20
38
// This is used to determine if a value is encrypted or not.
21
39
// If it is encrypted but a key is not provided, an error is returned.
40
+ // MagicPrefix will be followed by the first 7 characters of the cipher's
41
+ // SHA256 digest, followed by a dash, followed by the base64-encoded
42
+ // encrypted value.
22
43
const MagicPrefix = "dbcrypt-"
23
44
45
+ // MagicPrefixLength is the length of the entire prefix used to identify
46
+ // encrypted values.
47
+ const MagicPrefixLength = len (MagicPrefix ) + 8
48
+
24
49
// sentinelValue is the value that is stored in the database to indicate
25
- // whether encryption is enabled. If not enabled, the raw value is "coder".
26
- // Otherwise, the value is encrypted.
50
+ // whether encryption is enabled. If not enabled, the value either not
51
+ // present, or is the raw string "coder".
52
+ // Otherwise, the value must be the encrypted value of the string "coder"
53
+ // using the current cipher.
27
54
const sentinelValue = "coder"
28
55
29
- var ErrNotEnabled = xerrors .New ("encryption is not enabled" )
56
+ var (
57
+ ErrNotEnabled = xerrors .New ("encryption is not enabled" )
58
+ b64encode = base64 .StdEncoding .EncodeToString
59
+ b64decode = base64 .StdEncoding .DecodeString
60
+ )
61
+
62
+ // DecryptFailedError is returned when decryption fails.
63
+ // It unwraps to sql.ErrNoRows.
64
+ type DecryptFailedError struct {
65
+ Inner error
66
+ }
67
+
68
+ func (e * DecryptFailedError ) Error () string {
69
+ return xerrors .Errorf ("decrypt failed: %w" , e .Inner ).Error ()
70
+ }
71
+
72
+ func (* DecryptFailedError ) Unwrap () error {
73
+ return sql .ErrNoRows
74
+ }
75
+
76
+ func IsDecryptFailedError (err error ) bool {
77
+ var e * DecryptFailedError
78
+ return errors .As (err , & e )
79
+ }
30
80
31
81
type Options struct {
32
82
// ExternalTokenCipher is an optional cipher that is used
@@ -152,7 +202,7 @@ func (db *dbCrypt) encryptFields(fields ...*string) error {
152
202
return err
153
203
}
154
204
// Base64 is used to support UTF-8 encoding in PostgreSQL.
155
- * field = MagicPrefix + base64 . StdEncoding . EncodeToString (encrypted )
205
+ * field = MagicPrefix + cipher . HexDigest ()[: 7 ] + "-" + b64encode (encrypted )
156
206
}
157
207
return nil
158
208
}
@@ -181,15 +231,27 @@ func (db *dbCrypt) decryptFields(fields ...*string) error {
181
231
if field == nil {
182
232
continue
183
233
}
184
- if len (* field ) < len (MagicPrefix ) || ! strings .HasPrefix (* field , MagicPrefix ) {
234
+
235
+ if len (* field ) < 16 || ! strings .HasPrefix (* field , MagicPrefix ) {
185
236
// We do not force decryption of unencrypted rows. This could be damaging
186
237
// to the deployment, and admins can always manually purge data.
187
238
continue
188
239
}
189
- data , err := base64 .StdEncoding .DecodeString ((* field )[len (MagicPrefix ):])
240
+
241
+ // The first 7 characters of the digest are used to identify the cipher.
242
+ // If the cipher changes, we should complain loudly.
243
+ encPrefix := cipher .HexDigest ()[:7 ]
244
+ if ! strings .HasPrefix ((* field )[8 :15 ], encPrefix ) {
245
+ return & DecryptFailedError {
246
+ Inner : xerrors .Errorf ("cipher mismatch: expected %q, got %q" , encPrefix , (* field )[8 :15 ]),
247
+ }
248
+ }
249
+ data , err := b64decode ((* field )[16 :])
190
250
if err != nil {
191
251
// If it's not base64 with the prefix, we should complain loudly.
192
- return xerrors .Errorf ("malformed encrypted field %q: %w" , * field , err )
252
+ return & DecryptFailedError {
253
+ Inner : xerrors .Errorf ("malformed encrypted field %q: %w" , * field , err ),
254
+ }
193
255
}
194
256
decrypted , err := cipher .Decrypt (data )
195
257
if err != nil {
@@ -211,8 +273,7 @@ func ensureEncrypted(ctx context.Context, dbc *dbCrypt) error {
211
273
}
212
274
213
275
if val != "" && val != sentinelValue {
214
- // TODO: Handle key rotation.
215
- return xerrors .Errorf ("database is already encrypted with a different key and key rotation is not implemented yet" )
276
+ return xerrors .Errorf ("database is already encrypted with a different key" )
216
277
}
217
278
218
279
if val == sentinelValue {
0 commit comments