Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add gorm plugin for encrypted fields #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ module conjur-in-go
go 1.14

require (
github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.7.0 // indirect
gorm.io/driver/postgres v1.0.8
gorm.io/gorm v1.21.8
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand All @@ -35,6 +37,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
Expand Down Expand Up @@ -209,6 +212,7 @@ github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
Expand Down Expand Up @@ -259,6 +263,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
Expand Down Expand Up @@ -416,6 +422,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8=
gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
Expand Down
8 changes: 2 additions & 6 deletions pkg/model/secret.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package model

import (
"database/sql"
)

type Secret struct {
ResourceId string
Value sql.RawBytes
ResourceId string `silo:"aad"`
Value string `silo:"encrypted"`
}

func (s Secret) TableName() string {
Expand Down
9 changes: 2 additions & 7 deletions pkg/server/endpoints/get-secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,8 @@ func RegisterSecretReadEndpoint(server *server.Server) {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
secretValue, err := keystore.Cipher().Decrypt([]byte(secret.ResourceId), secret.Value)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}

writer.Write(secretValue)
writer.Write([]byte(secret.Value))
},
).Methods("GET")
}
}
18 changes: 8 additions & 10 deletions pkg/slosilo/store/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ func NewKeyStore(db *gorm.DB, dataKey []byte) (*KeyStore, error) {
return nil, err
}

return &KeyStore{
keystore := &KeyStore{
cipher: cipher,
db: db,
keysById: map[string]*Key{},
keysByFingerprint: map[string]*Key{},
}, nil
}

db.Use(NewPlugin(WithKeyStore(keystore)))
return keystore, nil
}

func (k KeyStore) fetchKey(query *StoredKey) (*Key, error) {
Expand All @@ -58,12 +61,7 @@ func (k KeyStore) fetchKey(query *StoredKey) (*Key, error) {
return nil, err
}

decryptedKey, err := k.cipher.Decrypt([]byte(storedKey.Id), storedKey.Key)
if err != nil {
return nil, err
}

keyInstance, err := slosilo.NewKey(decryptedKey)
keyInstance, err := slosilo.NewKey([]byte(storedKey.Key))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -99,5 +97,5 @@ func (k KeyStore) Cipher() *slosilo.Symmetric {
}

func (k KeyStore) ByAccount(account string) (*Key, error) {
return k.fetchKey(&StoredKey{Id: "authn:"+account})
}
return k.fetchKey(&StoredKey{Id: "authn:" + account})
}
8 changes: 2 additions & 6 deletions pkg/slosilo/store/model.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package store

import (
"database/sql"
)

type StoredKey struct {
Id string
Id string `silo:"aad"`
Fingerprint string
Key sql.RawBytes
Key string `silo:"encrypted"`
}

func (_ StoredKey) TableName() string {
Expand Down
164 changes: 164 additions & 0 deletions pkg/slosilo/store/silo-plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package store

import (
"conjur-in-go/pkg/slosilo"
"fmt"
"reflect"
"sort"
"strings"

"gorm.io/gorm"
)

type options struct {
// The key to use for encrypt/decrypt operations
keystore *KeyStore
}

// ApplyOption applies a give set of supplied options
type ApplyOption func(o *options)

type processor func(string, string) string

type siloPlugin struct {
opt *options
}

// WithKeyStore applies the supplied key to the options for use in
// encryption/decryption
func WithKeyStore(keystore *KeyStore) ApplyOption {
return func(o *options) {
o.keystore = keystore
}
}

func defaultOptions() *options {
return new(options)
}

// New constructs a new plugin based silo. It encrypts all secure labeled fields
// before write and decrypts after read.
func NewPlugin(opts ...ApplyOption) gorm.Plugin {
dst := defaultOptions()

for _, apply := range opts {
apply(dst)
}

return siloPlugin{
opt: dst,
}
}

func (s siloPlugin) Name() string {
return "silo"
}

func (s siloPlugin) encrypt(content string, additionalData string) string {
nonce, err := slosilo.RandomNonce()
if err != nil {
panic(err)
}

result, err := s.opt.keystore.Cipher().Encrypt([]byte(additionalData), []byte(content), nonce)
if err != nil {
panic(err)
}

return string(result)
}

func (s siloPlugin) decrypt(content string, additionalData string) string {
result, err := s.opt.keystore.Cipher().Decrypt([]byte(additionalData), []byte(content))
if err != nil {
panic(err)
}
return string(result)
}

func (s siloPlugin) Initialize(db *gorm.DB) (err error) {
db.Callback().Create().Before("gorm:create").Register("silo:before_create", s.encryptQuery)
db.Callback().Create().After("gorm:create").Register("silo:after_create", s.decryptQuery)
db.Callback().Update().Before("gorm:update").Register("silo:before_update", s.encryptQuery)
db.Callback().Query().After("gorm:query").Register("silo:after_query", s.decryptQuery)

return
}

func (s siloPlugin) encryptQuery(db *gorm.DB) {
s.processQuery(db, s.encrypt)
}

func (s siloPlugin) decryptQuery(db *gorm.DB) {
s.processQuery(db, s.decrypt)
}

func (s siloPlugin) processQuery(db *gorm.DB, fn processor) {
if db.Statement.Schema != nil {
switch db.Statement.ReflectValue.Kind() {
case reflect.Struct:
var destMap map[string]interface{}
if dest, ok := db.Statement.Dest.(map[string]interface{}); ok {
destMap = dest
}
s.processFields(db, db.Statement.ReflectValue, destMap, fn)
case reflect.Slice, reflect.Array:
var destMapList []map[string]interface{}
if dest, ok := db.Statement.Dest.([]map[string]interface{}); ok {
destMapList = dest
}
for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
var destMap map[string]interface{}
if i < len(destMapList) {
destMap = destMapList[i]
}
s.processFields(db, db.Statement.ReflectValue.Index(i), destMap, fn)
}
}
}
}

// decryptFields replaces the value on a returned record with the decrypted value
func (s siloPlugin) processFields(db *gorm.DB, reflectValue reflect.Value, dataDestination map[string]interface{}, fn processor) {
aad := getAdditionalData(db, reflectValue)
for _, field := range db.Statement.Schema.Fields {
if field.Tag.Get("silo") == "encrypted" {
if fieldValue, isZero := field.ValueOf(reflectValue); !isZero {
switch field.FieldType.Kind() {
case reflect.String:
// decrypted, err := s.opt.symmetric.Decrypt([]byte(aad), []byte(fieldValue.(string)))
result := fn(fieldValue.(string), aad)
field.Set(reflectValue, result)
if _, ok := dataDestination[field.Name]; ok {
dataDestination[field.Name] = result
}
default:
fmt.Printf("Unsupported encrypted field datatype: %+v\n", field)
}
}
}
}
}

// getAdditionalAdata assembles the string to use as additional data when encrypting/decrypting
// data based on schema field tags
func getAdditionalData(db *gorm.DB, reflectValue reflect.Value) (result string) {
var aadStrings []string
for _, field := range db.Statement.Schema.Fields {
if field.Tag.Get("silo") == "aad" {
if fieldValue, isZero := field.ValueOf(reflectValue); !isZero {
kind := field.FieldType.Kind()
switch kind {
case reflect.String:
aadStrings = append(aadStrings, fieldValue.(string))
default:
fmt.Printf("Unsupported additional data field datatype: %+v\n", field)
}
}
}
}
// Sort the strings so they have a stable order
sort.Strings(aadStrings)
result = strings.Join(aadStrings, "")
return
}
Loading