Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 23 additions & 104 deletions pkg/dbcrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

# dbcrypt Package Documentation

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 / decrypt sensitive data using gorm hooks (see example)
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.

## Example Usage

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

import (
"fmt"
"log"

"github.com/example/dbcrypt"
"github.com/greenbone/opensight-golang-libraries/pkg/dbcrypt"
)

type Person struct {
gorm.Model
Field1 string
PwdField string `encrypt:"true"`
PasswordField string `encrypt:"true"`
}

func (a *MyTable) encrypt(tx *gorm.DB) (err error) {
err = cryptor.EncryptStruct(a)
func main() {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
err := tx.AddError(fmt.Errorf("unable to encrypt password %w", err))
if err != nil {
return err
}
return err
log.Fatalf("Error %v", err)
}
return nil
}

func (a *MyTable) BeforeCreate(tx *gorm.DB) (err error) {
return a.encrypt(tx)
}

func (a *MyTable) AfterFind(tx *gorm.DB) (err error) {
err = cryptor.DecryptStruct(a)
cipher, err := dbcrypt.NewDBCipher(dbcrypt.Config{
Password: "password",
PasswordSalt: "password-salt-0123456789-0123456",
})
if err != nil {
err := tx.AddError(fmt.Errorf("Unable to decrypt password %w", err))
if err != nil {
return err
}
return err
log.Fatalf("Error %v", err)
}
return nil
}

```

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
activated.

---

<!-- gomarkdoc:embed:start -->

<!-- Code generated by gomarkdoc. DO NOT EDIT -->

# dbcrypt

```go
import "github.com/greenbone/opensight-golang-libraries/pkg/dbcrypt"
```

## Index

- [func Decrypt\(encrypted string, key \[\]byte\) \(string, error\)](<#Decrypt>)
- [func Encrypt\(plaintext string, key \[\]byte\) \(string, error\)](<#Encrypt>)
- [type DBCrypt](<#DBCrypt>)
- [func \(d \*DBCrypt\[T\]\) DecryptStruct\(data \*T\) error](<#DBCrypt[T].DecryptStruct>)
- [func \(d \*DBCrypt\[T\]\) EncryptStruct\(data \*T\) error](<#DBCrypt[T].EncryptStruct>)


<a name="Decrypt"></a>
## func Decrypt

```go
func Decrypt(encrypted string, key []byte) (string, error)
```



<a name="Encrypt"></a>
## func Encrypt

```go
func Encrypt(plaintext string, key []byte) (string, error)
```



<a name="DBCrypt"></a>
## type DBCrypt

dbcrypt.Register(db, cipher)

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

```go
type DBCrypt[T any] struct {
// contains filtered or unexported fields
personRead := &Person{}
if err := db.First(personRead).Error; err != nil {
log.Fatalf("Error %v", err)
}
}
```

<a name="DBCrypt[T].DecryptStruct"></a>
### func \(\*DBCrypt\[T\]\) DecryptStruct

```go
func (d *DBCrypt[T]) DecryptStruct(data *T) error
```

DecryptStruct decrypts all fields of a struct that are tagged with \`encrypt:"true"\`

<a name="DBCrypt[T].EncryptStruct"></a>
### func \(\*DBCrypt\[T\]\) EncryptStruct

```go
func (d *DBCrypt[T]) EncryptStruct(data *T) error
```

EncryptStruct encrypts all fields of a struct that are tagged with \`encrypt:"true"\`

Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)


<!-- gomarkdoc:embed:end -->
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.

# License

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

Licensed under the [GNU General Public License v3.0 or later](../../LICENSE).
Licensed under the [GNU General Public License v3.0 or later](../../LICENSE).
124 changes: 124 additions & 0 deletions pkg/dbcrypt/cipher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package dbcrypt

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"fmt"

"golang.org/x/crypto/argon2"
)

type dbCipher interface {
Encrypt(plaintext []byte) ([]byte, error)
Decrypt(ciphertext []byte) ([]byte, error)
}

type dbCipherGcmAes struct {
key []byte
}

func newDbCipherGcmAes(key []byte) dbCipher {
return dbCipherGcmAes{key: key}
}

func newDbCipherGcmAesWithoutKdf(password, passwordSalt string) dbCipher {
// Historically "v1" uses key truncation to 32 bytes. It needs to be preserved for backward compatibility.
key := make([]byte, 32)
copy(key, []byte(password+passwordSalt))
return newDbCipherGcmAes(key)
}

func newDbCipherGcmAesWithArgon2idKdf(password, passwordSalt string) dbCipher {
key := argon2.IDKey([]byte(password), []byte(passwordSalt), 1, 64*1024, 4, 32)
return newDbCipherGcmAes(key)
}

func (c dbCipherGcmAes) Encrypt(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("error creating AES cipher: %w", err)
}

gcm, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return nil, fmt.Errorf("error encrypting plaintext: %w", err)
}

ciphertext := gcm.Seal(nil, nil, []byte(plaintext), nil)
return ciphertext, nil
}

func (c dbCipherGcmAes) Decrypt(ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("error creating AES cipher: %w", err)
}

gcm, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return nil, fmt.Errorf("error decrypting ciphertext: %w", err)
}

plaintext, err := gcm.Open(nil, nil, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("error decrypting ciphertext: %w", err)
}

return plaintext, nil
}

type dbCipherHexEncode struct {
impl dbCipher
}

func newDbCipherHexEncode(impl dbCipher) dbCipher {
return dbCipherHexEncode{impl: impl}
}

func (c dbCipherHexEncode) Encrypt(plaintext []byte) ([]byte, error) {
ciphertext, err := c.impl.Encrypt(plaintext)
if err != nil {
return nil, err
}
encoded := hex.AppendEncode(nil, ciphertext)
return encoded, nil
}

func (c dbCipherHexEncode) Decrypt(encoded []byte) ([]byte, error) {
ciphertext, err := hex.AppendDecode(nil, encoded)
if err != nil {
return nil, fmt.Errorf("error decoding ciphertext: %w", err)
}
return c.impl.Decrypt(ciphertext)
}

type dbCipherBase64Encode struct {
impl dbCipher
}

func newDbCipherBase64Encode(impl dbCipher) dbCipher {
return dbCipherBase64Encode{impl: impl}
}

func (c dbCipherBase64Encode) Encrypt(plaintext []byte) ([]byte, error) {
ciphertext, err := c.impl.Encrypt(plaintext)
if err != nil {
return nil, err
}
encoded := base64.StdEncoding.AppendEncode(nil, ciphertext)
return encoded, nil
}

func (c dbCipherBase64Encode) Decrypt(encoded []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.AppendDecode(nil, encoded)
if err != nil {
return nil, fmt.Errorf("error decoding ciphertext: %w", err)
}
return c.impl.Decrypt(ciphertext)
}
110 changes: 110 additions & 0 deletions pkg/dbcrypt/cipher_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package dbcrypt

import (
"fmt"
"strings"
)

type cipherSpec struct {
Version string
Prefix string
Cipher dbCipher
}

func (cs *cipherSpec) Validate() error {
if cs.Version == "" {
return fmt.Errorf("version is missing")
}
if cs.Prefix == "" {
return fmt.Errorf("prefix is missing")
}
if strings.Contains(cs.Prefix, prefixSeparator) {
return fmt.Errorf("prefix cannot contain %q", prefixSeparator)
}
if cs.Cipher == nil {
return fmt.Errorf("cipher is missing")
}
return nil
}

type ciphersSpec struct {
DefaultVersion string
Ciphers []cipherSpec
}

func (cs *ciphersSpec) Validate() error {
if cs.DefaultVersion == "" {
return fmt.Errorf("default version is missing")
}

seenVersions := make(map[string]bool)
seenPrefix := make(map[string]bool)
defaultFound := false
for _, spec := range cs.Ciphers {
if err := spec.Validate(); err != nil {
return fmt.Errorf("cipher spec: %w", err)
}
if seenVersions[spec.Version] {
return fmt.Errorf("duplicate cipher spec version %q", spec.Version)
}
seenVersions[spec.Version] = true

if seenPrefix[spec.Prefix] {
return fmt.Errorf("duplicate cipher spec prefix %q", spec.Prefix)
}
seenPrefix[spec.Prefix] = true

if spec.Version == cs.DefaultVersion {
defaultFound = true
}
}
if !defaultFound {
return fmt.Errorf("default version %q not found in cipher specs", cs.DefaultVersion)
}

return nil
}

func newCiphersSpec(conf Config) (*ciphersSpec, error) {
cs := &ciphersSpec{
DefaultVersion: "v2",
Ciphers: []cipherSpec{ // /!\ this list can only be extended, otherwise decryption will break for existing data
{
Version: "v1",
Prefix: "ENC",
Cipher: newDbCipherHexEncode(newDbCipherGcmAesWithoutKdf(conf.Password, conf.PasswordSalt)),
},
{
Version: "v2",
Prefix: "ENCV2",
Cipher: newDbCipherBase64Encode(newDbCipherGcmAesWithArgon2idKdf(conf.Password, conf.PasswordSalt)),
},
},
}
if err := cs.Validate(); err != nil {
return nil, err
}
return cs, nil
}

func (cs *ciphersSpec) GetByVersion(version string) (*cipherSpec, error) {
for _, spec := range cs.Ciphers {
if spec.Version == version {
return &spec, nil
}
}
return nil, fmt.Errorf("cipher version %q not found", version)
}

func (cs *ciphersSpec) GetByPrefix(prefix string) (*cipherSpec, error) {
for _, spec := range cs.Ciphers {
if spec.Prefix == prefix {
return &spec, nil
}
}
return nil, fmt.Errorf("cipher prefix %q not found", prefix)
}
Loading
Loading