Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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: dbcrypt.NewEncryptedString("secret")}
Comment thread
badarghal marked this conversation as resolved.
Outdated
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).
148 changes: 148 additions & 0 deletions pkg/dbcrypt/cipher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2025 Greenbone AG <https://greenbone.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package dbcrypt

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

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

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

type dbCipherV1 struct {
key []byte
}

func newDbCipherV1(conf Config) (dbCipher, error) {
// Historically "v1" uses key truncation to 32 bytes. It needs to be preserved for backward compatibility.
key := []byte(conf.Password + conf.PasswordSalt)[:32]
return dbCipherV1{key: key}, nil
}

func (c dbCipherV1) Prefix() string {
return "ENC"
}

func (c dbCipherV1) Encrypt(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

iv := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}

ciphertext := gcm.Seal(nil, iv, []byte(plaintext), nil)
ciphertextWithIv := append(iv, ciphertext...)
encoded := hex.AppendEncode(nil, ciphertextWithIv)
return encoded, nil
}

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

if len(ciphertextWithIv) < aes.BlockSize+1 {
return nil, fmt.Errorf("ciphertext too short")
}

block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("error creating AES cipher: %w", err)
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

iv := ciphertextWithIv[:gcm.NonceSize()]
ciphertext := ciphertextWithIv[gcm.NonceSize():]

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

return plaintext, nil
}

type dbCipherV2 struct {
key []byte
}

func newDbCipherV2(conf Config) (dbCipher, error) {
// "v2" uses proper KDF (argon2id) to get the key
key := argon2.IDKey([]byte(conf.Password), []byte(conf.PasswordSalt), 1, 64*1024, 4, 32)
return dbCipherV2{key: key}, nil
}

func (c dbCipherV2) Prefix() string {
return "ENCV2"
}

func (c dbCipherV2) Encrypt(plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCMWithRandomNonce(block)
if err != nil {
return nil, err
}

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

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

if len(ciphertext) < aes.BlockSize+1 {
Comment thread
badarghal marked this conversation as resolved.
Outdated
return nil, fmt.Errorf("ciphertext too short")
}

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, err
}

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

return plaintext, nil
}
Loading
Loading