Skip to content

Commit 9556ab7

Browse files
Marek DalewskistefanTolksdorf
andauthored
change: introduce new implementation of dbcrypt package (#267)
## What It adds a new safer and easier to use implementation of `dbcrypt` package. ## Why Old implementation was riddled with bugs, security issues, it was hard to use and it was extremely easy to make mistakes and brake the system. ## References - [AT-2841](https://jira.greenbone.net/browse/AT-2841) - [AT-2246](https://jira.greenbone.net/browse/AT-2246) ## Checklist - [x] Tests ## Integration demonstration - https://github.com/greenbone/asset-management-backend/pull/1834 --------- Co-authored-by: Stefan Tolksdorf <90245064+stefanTolksdorf@users.noreply.github.com>
1 parent 9e0e3e0 commit 9556ab7

File tree

10 files changed

+913
-388
lines changed

10 files changed

+913
-388
lines changed

pkg/dbcrypt/README.md

Lines changed: 23 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
# dbcrypt Package Documentation
44

5-
This package provides functions for encrypting and decrypting fields of entities persisted with GORM
6-
using the AES algorithm. It uses the GCM mode of operation for encryption, which provides authentication and integrity
7-
protection for the encrypted data.
8-
It can be used to encrypt / decrypt sensitive data using gorm hooks (see example)
5+
This package provides functions for encrypting and decrypting fields of entities persisted with GORM using the AES algorithm. It uses the GCM mode of operation for encryption, which provides authentication and integrity protection for the encrypted data. It can be used to encrypt and decrypt sensitive data using gorm hooks.
96

107
## Example Usage
118

@@ -15,125 +12,47 @@ Here is an example of how to use the dbcrypt package:
1512
package main
1613

1714
import (
18-
"fmt"
15+
"log"
1916

20-
"github.com/example/dbcrypt"
17+
"github.com/greenbone/opensight-golang-libraries/pkg/dbcrypt"
2118
)
2219

2320
type Person struct {
2421
gorm.Model
25-
Field1 string
26-
PwdField string `encrypt:"true"`
22+
PasswordField string `encrypt:"true"`
2723
}
2824

29-
func (a *MyTable) encrypt(tx *gorm.DB) (err error) {
30-
err = cryptor.EncryptStruct(a)
25+
func main() {
26+
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
3127
if err != nil {
32-
err := tx.AddError(fmt.Errorf("unable to encrypt password %w", err))
33-
if err != nil {
34-
return err
35-
}
36-
return err
28+
log.Fatalf("Error %v", err)
3729
}
38-
return nil
39-
}
40-
41-
func (a *MyTable) BeforeCreate(tx *gorm.DB) (err error) {
42-
return a.encrypt(tx)
43-
}
4430

45-
func (a *MyTable) AfterFind(tx *gorm.DB) (err error) {
46-
err = cryptor.DecryptStruct(a)
31+
cipher, err := dbcrypt.NewDBCipher(dbcrypt.Config{
32+
Password: "password",
33+
PasswordSalt: "password-salt-0123456789-0123456",
34+
})
4735
if err != nil {
48-
err := tx.AddError(fmt.Errorf("Unable to decrypt password %w", err))
49-
if err != nil {
50-
return err
51-
}
52-
return err
36+
log.Fatalf("Error %v", err)
5337
}
54-
return nil
55-
}
56-
57-
```
58-
59-
In this example, a Person struct is created and encrypted using the DBCrypt struct. The encrypted struct is then saved to the database. Finally the struct is decrypted when the gorm hook is
60-
activated.
61-
62-
---
63-
64-
<!-- gomarkdoc:embed:start -->
65-
66-
<!-- Code generated by gomarkdoc. DO NOT EDIT -->
67-
68-
# dbcrypt
69-
70-
```go
71-
import "github.com/greenbone/opensight-golang-libraries/pkg/dbcrypt"
72-
```
73-
74-
## Index
75-
76-
- [func Decrypt\(encrypted string, key \[\]byte\) \(string, error\)](<#Decrypt>)
77-
- [func Encrypt\(plaintext string, key \[\]byte\) \(string, error\)](<#Encrypt>)
78-
- [type DBCrypt](<#DBCrypt>)
79-
- [func \(d \*DBCrypt\[T\]\) DecryptStruct\(data \*T\) error](<#DBCrypt[T].DecryptStruct>)
80-
- [func \(d \*DBCrypt\[T\]\) EncryptStruct\(data \*T\) error](<#DBCrypt[T].EncryptStruct>)
81-
82-
83-
<a name="Decrypt"></a>
84-
## func Decrypt
85-
86-
```go
87-
func Decrypt(encrypted string, key []byte) (string, error)
88-
```
89-
90-
91-
92-
<a name="Encrypt"></a>
93-
## func Encrypt
94-
95-
```go
96-
func Encrypt(plaintext string, key []byte) (string, error)
97-
```
98-
99-
100-
101-
<a name="DBCrypt"></a>
102-
## type DBCrypt
103-
38+
dbcrypt.Register(db, cipher)
10439

40+
personWrite := &Person{PasswordField: "secret"}
41+
if err := db.Create(personWrite).Error; err != nil {
42+
log.Fatalf("Error %v", err)
43+
}
10544

106-
```go
107-
type DBCrypt[T any] struct {
108-
// contains filtered or unexported fields
45+
personRead := &Person{}
46+
if err := db.First(personRead).Error; err != nil {
47+
log.Fatalf("Error %v", err)
48+
}
10949
}
11050
```
11151

112-
<a name="DBCrypt[T].DecryptStruct"></a>
113-
### func \(\*DBCrypt\[T\]\) DecryptStruct
114-
115-
```go
116-
func (d *DBCrypt[T]) DecryptStruct(data *T) error
117-
```
118-
119-
DecryptStruct decrypts all fields of a struct that are tagged with \`encrypt:"true"\`
120-
121-
<a name="DBCrypt[T].EncryptStruct"></a>
122-
### func \(\*DBCrypt\[T\]\) EncryptStruct
123-
124-
```go
125-
func (d *DBCrypt[T]) EncryptStruct(data *T) error
126-
```
127-
128-
EncryptStruct encrypts all fields of a struct that are tagged with \`encrypt:"true"\`
129-
130-
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
131-
132-
133-
<!-- gomarkdoc:embed:end -->
52+
In this example, a Person struct is created and `PasswordField` is automatically encrypted before storing in the database using the DBCipher. Then, when the data is retrieved from the database `PasswordField` is automatically decrypted.
13453

13554
# License
13655

13756
Copyright (C) 2022-2023 [Greenbone AG][Greenbone AG]
13857

139-
Licensed under the [GNU General Public License v3.0 or later](../../LICENSE).
58+
Licensed under the [GNU General Public License v3.0 or later](../../LICENSE).

pkg/dbcrypt/cipher.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
package dbcrypt
6+
7+
import (
8+
"crypto/aes"
9+
"crypto/cipher"
10+
"encoding/base64"
11+
"encoding/hex"
12+
"fmt"
13+
14+
"golang.org/x/crypto/argon2"
15+
)
16+
17+
type dbCipher interface {
18+
Encrypt(plaintext []byte) ([]byte, error)
19+
Decrypt(ciphertext []byte) ([]byte, error)
20+
}
21+
22+
type dbCipherGcmAes struct {
23+
key []byte
24+
}
25+
26+
func newDbCipherGcmAes(key []byte) dbCipher {
27+
return dbCipherGcmAes{key: key}
28+
}
29+
30+
func newDbCipherGcmAesWithoutKdf(password, passwordSalt string) dbCipher {
31+
// Historically "v1" uses key truncation to 32 bytes. It needs to be preserved for backward compatibility.
32+
key := make([]byte, 32)
33+
copy(key, []byte(password+passwordSalt))
34+
return newDbCipherGcmAes(key)
35+
}
36+
37+
func newDbCipherGcmAesWithArgon2idKdf(password, passwordSalt string) dbCipher {
38+
key := argon2.IDKey([]byte(password), []byte(passwordSalt), 1, 64*1024, 4, 32)
39+
return newDbCipherGcmAes(key)
40+
}
41+
42+
func (c dbCipherGcmAes) Encrypt(plaintext []byte) ([]byte, error) {
43+
block, err := aes.NewCipher(c.key)
44+
if err != nil {
45+
return nil, fmt.Errorf("error creating AES cipher: %w", err)
46+
}
47+
48+
gcm, err := cipher.NewGCMWithRandomNonce(block)
49+
if err != nil {
50+
return nil, fmt.Errorf("error encrypting plaintext: %w", err)
51+
}
52+
53+
ciphertext := gcm.Seal(nil, nil, []byte(plaintext), nil)
54+
return ciphertext, nil
55+
}
56+
57+
func (c dbCipherGcmAes) Decrypt(ciphertext []byte) ([]byte, error) {
58+
block, err := aes.NewCipher(c.key)
59+
if err != nil {
60+
return nil, fmt.Errorf("error creating AES cipher: %w", err)
61+
}
62+
63+
gcm, err := cipher.NewGCMWithRandomNonce(block)
64+
if err != nil {
65+
return nil, fmt.Errorf("error decrypting ciphertext: %w", err)
66+
}
67+
68+
plaintext, err := gcm.Open(nil, nil, ciphertext, nil)
69+
if err != nil {
70+
return nil, fmt.Errorf("error decrypting ciphertext: %w", err)
71+
}
72+
73+
return plaintext, nil
74+
}
75+
76+
type dbCipherHexEncode struct {
77+
impl dbCipher
78+
}
79+
80+
func newDbCipherHexEncode(impl dbCipher) dbCipher {
81+
return dbCipherHexEncode{impl: impl}
82+
}
83+
84+
func (c dbCipherHexEncode) Encrypt(plaintext []byte) ([]byte, error) {
85+
ciphertext, err := c.impl.Encrypt(plaintext)
86+
if err != nil {
87+
return nil, err
88+
}
89+
encoded := hex.AppendEncode(nil, ciphertext)
90+
return encoded, nil
91+
}
92+
93+
func (c dbCipherHexEncode) Decrypt(encoded []byte) ([]byte, error) {
94+
ciphertext, err := hex.AppendDecode(nil, encoded)
95+
if err != nil {
96+
return nil, fmt.Errorf("error decoding ciphertext: %w", err)
97+
}
98+
return c.impl.Decrypt(ciphertext)
99+
}
100+
101+
type dbCipherBase64Encode struct {
102+
impl dbCipher
103+
}
104+
105+
func newDbCipherBase64Encode(impl dbCipher) dbCipher {
106+
return dbCipherBase64Encode{impl: impl}
107+
}
108+
109+
func (c dbCipherBase64Encode) Encrypt(plaintext []byte) ([]byte, error) {
110+
ciphertext, err := c.impl.Encrypt(plaintext)
111+
if err != nil {
112+
return nil, err
113+
}
114+
encoded := base64.StdEncoding.AppendEncode(nil, ciphertext)
115+
return encoded, nil
116+
}
117+
118+
func (c dbCipherBase64Encode) Decrypt(encoded []byte) ([]byte, error) {
119+
ciphertext, err := base64.StdEncoding.AppendDecode(nil, encoded)
120+
if err != nil {
121+
return nil, fmt.Errorf("error decoding ciphertext: %w", err)
122+
}
123+
return c.impl.Decrypt(ciphertext)
124+
}

pkg/dbcrypt/cipher_spec.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
package dbcrypt
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
)
11+
12+
type cipherSpec struct {
13+
Version string
14+
Prefix string
15+
Cipher dbCipher
16+
}
17+
18+
func (cs *cipherSpec) Validate() error {
19+
if cs.Version == "" {
20+
return fmt.Errorf("version is missing")
21+
}
22+
if cs.Prefix == "" {
23+
return fmt.Errorf("prefix is missing")
24+
}
25+
if strings.Contains(cs.Prefix, prefixSeparator) {
26+
return fmt.Errorf("prefix cannot contain %q", prefixSeparator)
27+
}
28+
if cs.Cipher == nil {
29+
return fmt.Errorf("cipher is missing")
30+
}
31+
return nil
32+
}
33+
34+
type ciphersSpec struct {
35+
DefaultVersion string
36+
Ciphers []cipherSpec
37+
}
38+
39+
func (cs *ciphersSpec) Validate() error {
40+
if cs.DefaultVersion == "" {
41+
return fmt.Errorf("default version is missing")
42+
}
43+
44+
seenVersions := make(map[string]bool)
45+
seenPrefix := make(map[string]bool)
46+
defaultFound := false
47+
for _, spec := range cs.Ciphers {
48+
if err := spec.Validate(); err != nil {
49+
return fmt.Errorf("cipher spec: %w", err)
50+
}
51+
if seenVersions[spec.Version] {
52+
return fmt.Errorf("duplicate cipher spec version %q", spec.Version)
53+
}
54+
seenVersions[spec.Version] = true
55+
56+
if seenPrefix[spec.Prefix] {
57+
return fmt.Errorf("duplicate cipher spec prefix %q", spec.Prefix)
58+
}
59+
seenPrefix[spec.Prefix] = true
60+
61+
if spec.Version == cs.DefaultVersion {
62+
defaultFound = true
63+
}
64+
}
65+
if !defaultFound {
66+
return fmt.Errorf("default version %q not found in cipher specs", cs.DefaultVersion)
67+
}
68+
69+
return nil
70+
}
71+
72+
func newCiphersSpec(conf Config) (*ciphersSpec, error) {
73+
cs := &ciphersSpec{
74+
DefaultVersion: "v2",
75+
Ciphers: []cipherSpec{ // /!\ this list can only be extended, otherwise decryption will break for existing data
76+
{
77+
Version: "v1",
78+
Prefix: "ENC",
79+
Cipher: newDbCipherHexEncode(newDbCipherGcmAesWithoutKdf(conf.Password, conf.PasswordSalt)),
80+
},
81+
{
82+
Version: "v2",
83+
Prefix: "ENCV2",
84+
Cipher: newDbCipherBase64Encode(newDbCipherGcmAesWithArgon2idKdf(conf.Password, conf.PasswordSalt)),
85+
},
86+
},
87+
}
88+
if err := cs.Validate(); err != nil {
89+
return nil, err
90+
}
91+
return cs, nil
92+
}
93+
94+
func (cs *ciphersSpec) GetByVersion(version string) (*cipherSpec, error) {
95+
for _, spec := range cs.Ciphers {
96+
if spec.Version == version {
97+
return &spec, nil
98+
}
99+
}
100+
return nil, fmt.Errorf("cipher version %q not found", version)
101+
}
102+
103+
func (cs *ciphersSpec) GetByPrefix(prefix string) (*cipherSpec, error) {
104+
for _, spec := range cs.Ciphers {
105+
if spec.Prefix == prefix {
106+
return &spec, nil
107+
}
108+
}
109+
return nil, fmt.Errorf("cipher prefix %q not found", prefix)
110+
}

0 commit comments

Comments
 (0)