From b0085804d1f0db181832e2768bd0f7cb4cdd2bc7 Mon Sep 17 00:00:00 2001 From: srgdemon Date: Fri, 14 Aug 2026 14:06:32 +0300 Subject: [PATCH 1/2] pkg/presign: cryptobox preferred --- pkg/presign/crypto.go | 184 +++++++++++++++++++++++++++ pkg/presign/go.mod | 10 +- pkg/presign/go.sum | 8 ++ pkg/presign/presign.go | 4 +- pkg/presign/presign_test.go | 244 ++++++++++++++++++++++++++++++++++++ pkg/presign/v2.go | 90 +++++++++++++ 6 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 pkg/presign/crypto.go create mode 100644 pkg/presign/go.sum create mode 100644 pkg/presign/presign_test.go create mode 100644 pkg/presign/v2.go diff --git a/pkg/presign/crypto.go b/pkg/presign/crypto.go new file mode 100644 index 00000000..1d4ebca0 --- /dev/null +++ b/pkg/presign/crypto.go @@ -0,0 +1,184 @@ +package presign + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "fmt" + "strconv" + + "github.com/webitel/crypto/cryptobox" +) + +// modern cryptobox.Cipher implementation +type Crypto struct { + box cryptobox.Cipher +} + +// NewCryptoBox implements modern ciphertext encryption strategy +func NewCryptoBox(box cryptobox.Cipher) (PreSign, error) { + cbox := Crypto{box} + err := cbox.init() + if err != nil { + return nil, err + } + return cbox, nil +} + +// lazy init +func (c *Crypto) init() (err error) { + if c.box != nil { + return nil // already + } + // Load environment configuration + c.box, err = cryptobox.Default() + if err != nil { + return err + } + // OK + return nil +} + +var _ PreSign = Crypto{} + +var hashSum = hash + +func (c Crypto) Generate(data []byte) (string, error) { + + err := c.init() + if err != nil { + return "", err + } + + sign := hashSum(data) + sign, err = c.box.Encrypt( + context.Background(), sign, + ) + if err != nil { + // failed to encrypt hash of the given data message + return "", err + } + + return hex.EncodeToString(sign), nil +} + +func (c Crypto) Valid(plaintext string, signature string) bool { + + sign, err := hex.DecodeString(signature) + if err != nil { + // failed to decode signature + return false + } + + err = c.init() + if err != nil { + return false + } + + // v2 + sign, err = c.box.Decrypt( + context.Background(), sign, + ) + if err != nil { + // failed to decrypt signature + return false + } + + // verify + want := hashSum([]byte(plaintext)) + return bytes.Equal(want, sign) +} + +func (c Crypto) EncryptId(id int64) (string, error) { + text, err := c.encryptText( + binary.AppendVarint(nil, id), + ) + if err != nil { + return "", err + } + return string(text), nil +} + +func (c Crypto) DecryptId(key string) (int64, error) { + // v2 encryption + data, err := c.decryptText([]byte(key)) + if err != nil { + return 0, err + } + num, n := binary.Varint(data) + // if n <= 0 { + // // invalid input ; not integer encrypted + // return 0, strconv.ErrSyntax + // } + if n != len(data) { + // read too short + return 0, strconv.ErrSyntax + } + return num, nil +} + +func (c Crypto) EncryptBytes(v []byte) ([]byte, error) { + // from business logic: output supposed to be the cipher TEXT (printable, NOT raw bytes) + return c.encryptText(v) +} + +func (c Crypto) DecryptBytes(v []byte) ([]byte, error) { + // expect TEXT bytes ; see: c.EncryptBytes() + return c.decryptText(v) +} + +const cipherTag = ".c1" +var cipherText = base64.RawURLEncoding + +func (c *Crypto) encryptText(data []byte) (text []byte, err error) { + + err = c.init() + if err != nil { + return nil, err + } + + blob, err := c.box.Encrypt( + context.Background(), data, + ) + if err != nil { + // failed to encrypt sensitive data + return nil, err + } + + // v2 + // return cipherTag + cipherText.EncodeToString(blob), nil + return cipherText.AppendEncode([]byte(cipherTag), blob), nil +} + +func (c *Crypto) decryptText(text []byte) (data []byte, err error) { + + text, v2 := bytes.CutPrefix(text, []byte(cipherTag)) + if !v2 { + return nil, fmt.Errorf("presign: invalid syntax") + } + + blob, err := cipherText.AppendDecode(nil, text) + if err != nil { + // failed to decode ciphertext + return nil, err + } + + err = c.init() + if err != nil { + return nil, err + } + + data, err = c.box.Decrypt( + context.Background(), blob, + ) + if err != nil { + // failed to decrypt cipherdata + return nil, err + } + + // OK + return data, nil +} + diff --git a/pkg/presign/go.mod b/pkg/presign/go.mod index 08157bf5..189c14c6 100644 --- a/pkg/presign/go.mod +++ b/pkg/presign/go.mod @@ -1,3 +1,11 @@ module github.com/webitel/engine/pkg/presign -go 1.24.1 +go 1.25.0 + +require github.com/webitel/crypto/cryptobox v0.2.0 + +require ( + github.com/webitel/crypto/env v0.1.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/sys v0.47.0 // indirect +) diff --git a/pkg/presign/go.sum b/pkg/presign/go.sum new file mode 100644 index 00000000..1e925c91 --- /dev/null +++ b/pkg/presign/go.sum @@ -0,0 +1,8 @@ +github.com/webitel/crypto/cryptobox v0.2.0 h1:6KUza8aa9eBreI3aUFLJjkssvi8LYANcemM0vSBuNVE= +github.com/webitel/crypto/cryptobox v0.2.0/go.mod h1:td/OI3xSEbMl+gW3b84QESdR20uD3KHHsma59ke7mmQ= +github.com/webitel/crypto/env v0.1.0 h1:bIWQgAwPcThnuX6gOQmWUubVTZrnnavukya8dSXZwRc= +github.com/webitel/crypto/env v0.1.0/go.mod h1:/jyzvmqAY6KGRwiV4oCWaBAeZOW51j1JdgswUnIPxqo= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/pkg/presign/presign.go b/pkg/presign/presign.go index 0b7b9a01..d0f907d0 100644 --- a/pkg/presign/presign.go +++ b/pkg/presign/presign.go @@ -40,7 +40,7 @@ func hash(msg []byte) []byte { return hash } -func NewPreSigned(pemLocation string) (PreSign, error) { +func NewPrivateKey(pemLocation string) (PreSign, error) { var pkey *rsa.PrivateKey cert, err := ioutil.ReadFile(pemLocation) if err != nil { @@ -63,7 +63,7 @@ func NewPreSigned(pemLocation string) (PreSign, error) { case "RSA PRIVATE KEY": pkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) default: - return nil, errors.New(fmt.Sprintf("Unknown block type \"%s\"", block.Type)) + return nil, fmt.Errorf("Unknown block type \"%s\"", block.Type) } if err != nil { diff --git a/pkg/presign/presign_test.go b/pkg/presign/presign_test.go new file mode 100644 index 00000000..7c65bbc9 --- /dev/null +++ b/pkg/presign/presign_test.go @@ -0,0 +1,244 @@ +package presign_test + +import ( + "bytes" + "os" + "testing" + + "github.com/webitel/engine/pkg/presign" +) + +func TestMain(test *testing.M) { + + // export := os.Setenv + + // export("WBTL_CRYPTO_DIR", "") // modern + // export(testEnvPrivateKey, "") // legacy + + test.Run() +} + +const testEnvPrivateKey = "PRESIGN_KEYFILE" + +func legacy(test *testing.T) (presign.PreSign) { + test.Helper() + + keyfile := os.Getenv(testEnvPrivateKey) + if keyfile == "" { + test.Skipf("%s = ?", testEnvPrivateKey) + return nil + } + + impl, err := presign.NewPrivateKey(keyfile) + if err != nil { + test.Errorf("legacy: configuration failed ; %v", err) + } + return impl +} + +func modern(test *testing.T) (presign.PreSign) { + test.Helper() + + cbox, err := presign.NewCryptoBox(nil) + if err != nil { + test.Errorf("modern: configuration failed ; %v", err) + } + return cbox +} + +func latest(test *testing.T) (presign.PreSign) { + test.Helper() + + impl, err := presign.NewPreSigned( + os.Getenv(testEnvPrivateKey), + ) + if err != nil { + test.Errorf("latest: configuration failed ; %v", err) + } + return impl +} + +func TestDummy(test *testing.T) { + v0, v1 := legacy(test), modern(test) + + const inputText = "https://example.org/files/2026/08/03/image_20260803_153728.jpg" + + sign0, err := v0.Generate([]byte(inputText)) + if err != nil { + test.Errorf("pkey.Generate: %v", err) + } + + sign1, err := v1.Generate([]byte(inputText)) + if err != nil { + test.Errorf("cbox.Generate: %v", err) + } + + blob0, err := v0.EncryptBytes([]byte(inputText)) + if err != nil { + test.Errorf("pkey.EncryptBytes: %v", err) + } + + blob1, err := v1.EncryptBytes([]byte(inputText)) + if err != nil { + test.Errorf("cbox.EncryptBytes: %v", err) + } + + test.Logf("[input]: %s", inputText) + test.Logf("[pkey.Generate]: %s", sign0) + test.Logf("[cbox.Generate]: %s", sign1) + test.Logf("[pkey.EncryptBytes]: %s", blob0) + test.Logf("[cbox.EncryptBytes]: %s", blob1) + +} + +func TestSignatureRoundTrip(test *testing.T) { + + tests := []struct{ + name string + build func(*testing.T) presign.PreSign + }{ + {"pkey", legacy}, + {"cbox", modern}, + } + + const inputText = "https://example.org/files/2026/08/03/image_20260803_153728.jpg" + test.Logf("[plaintext]: %s", inputText) + + for _, tcase := range tests { + test.Run(tcase.name, func(t *testing.T) { + + enc := tcase.build(t) + + sign, err := enc.Generate([]byte(inputText)) + if err != nil { + test.Errorf("Generate: %v", err) + } + test.Logf("[Signature]: %s", sign) + if !enc.Valid(inputText, sign) { + test.Errorf("Verify: %v", err) + } + // // make invalid + // sign[len(sign)-1] = '0' + }) + } + +} + +func TestEncryptionRoundTrip(test *testing.T) { + + tests := []struct{ + name string + build func(*testing.T) presign.PreSign + }{ + {"pkey", legacy}, + {"cbox", modern}, + } + + const plaintext = "https://example.org/files/2026/08/03/image_20260803_153728.jpg" + test.Logf("[plaintext]: %s", plaintext) + + src := []byte(plaintext) + for _, tcase := range tests { + test.Run(tcase.name, func(t *testing.T) { + + enc := tcase.build(t) + + blob, err := enc.EncryptBytes(src) + if err != nil { + t.Errorf("EncryptBytes: %v", err) + } + t.Logf("EncryptBytes: %s", blob) + dst, err := enc.DecryptBytes(blob) + if err != nil { + t.Errorf("DecryptBytes: %v", err) + } + + if !bytes.Equal(dst, src) { + test.Errorf("DecryptBytes: unexpected data") + } + // // make invalid + // sign[len(sign)-1] = '0' + }) + } + +} + + +func TestNumberEncryptionRoundTrip(test *testing.T) { + + tests := []struct{ + name string + build func(*testing.T) presign.PreSign + }{ + {"pkey", legacy}, + {"cbox", modern}, + } + + const oid int64 = 56843 + test.Logf("[int64]: %d", oid) + + for _, tcase := range tests { + test.Run(tcase.name, func(t *testing.T) { + + enc := tcase.build(t) + + blob, err := enc.EncryptId(oid) + if err != nil { + t.Errorf("EncryptId: %v", err) + } + t.Logf("EncryptId: %s", blob) + got, err := enc.DecryptId(blob) + if err != nil { + t.Errorf("DecryptId: %v", err) + } + + if got != oid { + test.Errorf("decrypted number MUST be equal") + } + // // make invalid + // sign[len(sign)-1] = '0' + }) + } + +} + +func _TestDecryptBytes(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + blob []byte + want []byte + wantErr bool + }{ + // TODO: Add test cases. + // {"0", []byte("Em2vIKUfQu0DCuSbn3NxbcZui/Csonr3uKCwUKe6uZc2d8wgtU5FG6M9dmQHYyHZLRQ"), []byte(""), false}, + // {"1", []byte("fUoW98cD2eb7mZk+MBME/jrClhFBe9jhSA"), []byte(""), false}, + // {"2", []byte("Q+gfDweWVnpBk/EyFF6IGo2t5yWLqPQ"), []byte(""), false}, + // {"3", []byte("xseElYDZHJtIDTMmkmGT/qHrn90nwvo"), []byte(""), false}, + // {"4", []byte("2kGCoOQRNmCbMNP0Wu2ARIHMvM0R2gUZIA"), []byte(""), false}, + // {"5", []byte("qNSzUoo+orLnc541Y+BVZlbTx9gN"), []byte(""), false}, + // {"6", []byte("QLBp7rQ4pRBq3am6nHUKtCpBMP7R"), []byte(""), false}, + {"7", []byte("dQzXsQVU0tMuwaWaCcgCYgrfmjGKrsy3"), []byte(`"qwerty"`), false}, + } + + enc := latest(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := enc.DecryptBytes(tt.blob) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("DecryptBytes() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("DecryptBytes() succeeded unexpectedly") + } + t.Logf("DecryptBytes() = %s", got) + // TODO: update the condition below to compare got with tt.want. + if !bytes.Equal(got, tt.want) { + t.Errorf("DecryptBytes() = %v, want %v", got, tt.want) + } + }) + } +} \ No newline at end of file diff --git a/pkg/presign/v2.go b/pkg/presign/v2.go new file mode 100644 index 00000000..a03955cc --- /dev/null +++ b/pkg/presign/v2.go @@ -0,0 +1,90 @@ +package presign + +import "cmp" + +// NewPreSigned builds PreSign implementation with cryptobox preferred algorithm. +// Also supports legacy PrivateKey mechanism for signature verification & decryption. +// Use [NewPrivateKey] constructor for previous implementation only. +func NewPreSigned(pemLocation string) (PreSign, error) { + // MUST: v1. MODERN + var modern Crypto + err := modern.init() + if err != nil { + // MUST. -but- failed + return nil, err + } + // WITH: v0. LEGACY ? + if pemLocation == "" { + // ONLY: v1 (modern) support + return preferred{modern}, nil + } + // MUST v0 (legacy) support + legacy, err := NewPrivateKey(pemLocation) + if err != nil { + // bad configuration + return nil, err + } + // WITH: v0 (legacy) support + return preferred{modern, legacy}, nil +} + +// .well-known & supported +// [0] - encoding ; preferred +// [1:] - decoding ; supported +type preferred []PreSign + +func (x preferred) latest() PreSign { + return x[0] +} + +var _ PreSign = (preferred)(nil) + +func (x preferred) Generate(data []byte) (string, error) { + return x.latest().Generate(data) +} + +func (x preferred) Valid(plaintext string, signature string) bool { + for _, spec := range x { + if spec.Valid(plaintext, signature) { + return true // break + } + } + return false +} + +func (x preferred) EncryptId(id int64) (string, error) { + return x.latest().EncryptId(id) +} + +func (x preferred) DecryptId(key string) (int64, error) { + var err error // remember: first (latest) + for _, sup := range x { + oid, res := sup.DecryptId(key) + if res == nil { + // success + return oid, nil + } + // failure + err = cmp.Or(err, res) // first + } + return 0, err +} + +func (x preferred) EncryptBytes(data []byte) ([]byte, error) { + return x.latest().EncryptBytes(data) +} + +func (x preferred) DecryptBytes(text []byte) ([]byte, error) { + var err error // remember: first (latest) + for _, sup := range x { + data, res := sup.DecryptBytes(text) + if res == nil { + // success + return data, nil + } + // failure + err = cmp.Or(err, res) // first + } + return nil, err +} + From 0203e0cec248b3b5e9a05e557ab60eadeaae2a7c Mon Sep 17 00:00:00 2001 From: srgdemon Date: Fri, 14 Aug 2026 14:19:23 +0300 Subject: [PATCH 2/2] [WTEL-9696] cryptostore encryption --- app/app.go | 5 + go.mod | 16 +- go.sum | 32 ++-- model/email_profile.go | 8 +- store/sqlstore/crypto_schema.go | 75 ++++++++ store/sqlstore/crypto_types.go | 246 ++++++++++++++++++++++++++ store/sqlstore/email_profile_store.go | 52 +++--- store/sqlstore/supplier.go | 22 ++- 8 files changed, 411 insertions(+), 45 deletions(-) create mode 100644 store/sqlstore/crypto_schema.go create mode 100644 store/sqlstore/crypto_types.go diff --git a/app/app.go b/app/app.go index 1c621785..af438265 100644 --- a/app/app.go +++ b/app/app.go @@ -164,6 +164,11 @@ func New(options ...string) (outApp *App, outErr error) { } } + // failfast: load sqlstore/cryptostore.Codec from environment + if err = sqlstore.CryptoInit(); err != nil { + return nil, err + } + app.Store = store.NewLayeredStore(sqlstore.NewSqlSupplier(app.Config().SqlSettings)) app.MessageQueue = rabbit.NewRabbitMQ(app.Config().NodeName, &app.Config().MessageQueueSettings) diff --git a/go.mod b/go.mod index 935b8b6e..4da75ee1 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,9 @@ require ( github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 github.com/rabbitmq/amqp091-go v1.10.0 + github.com/webitel/crypto/cryptostore v0.2.1 github.com/webitel/engine/pkg/discovery v0.0.0-20250925090335-284caa978daa - github.com/webitel/engine/pkg/presign v0.0.0-20250507123601-4e6943ad1e27 + github.com/webitel/engine/pkg/presign v0.0.0-20260814110632-b0085804d1f0 github.com/webitel/engine/pkg/wbt v0.0.0-20250801070656-122a5f61b06a github.com/webitel/engine/pkg/werror v0.0.0-20250508121332-6ae1563235d8 github.com/webitel/webitel-go-kit v0.0.13-0.20240908192731-3abe573c0e41 @@ -33,9 +34,9 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 go.uber.org/atomic v1.11.0 go.uber.org/ratelimit v0.2.0 - golang.org/x/net v0.43.0 + golang.org/x/net v0.56.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.16.0 + golang.org/x/sync v0.22.0 google.golang.org/api v0.170.0 google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb google.golang.org/grpc v1.72.1 @@ -84,6 +85,9 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/poy/onpar v1.1.2 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/webitel/crypto/cryptobox v0.2.0 // indirect + github.com/webitel/crypto/encoding/jsonc v0.1.0 // indirect + github.com/webitel/crypto/env v0.1.0 // indirect github.com/ziutek/mymysql v1.5.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -106,10 +110,10 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20250808145144-a408d31f581a // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine/v2 v2.0.2 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect diff --git a/go.sum b/go.sum index ffbae87c..49f59705 100644 --- a/go.sum +++ b/go.sum @@ -331,8 +331,16 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/webitel/engine/pkg/presign v0.0.0-20250507123601-4e6943ad1e27 h1:DeL310n2Tx91iQvvFFQkY4EHMoUhK1pvmh7gOmp1eFw= -github.com/webitel/engine/pkg/presign v0.0.0-20250507123601-4e6943ad1e27/go.mod h1:C5rpf4XfdQ6a5+MpL0Ix0SsH/RNcrerpzqSomDH3VQY= +github.com/webitel/crypto/cryptobox v0.2.0 h1:6KUza8aa9eBreI3aUFLJjkssvi8LYANcemM0vSBuNVE= +github.com/webitel/crypto/cryptobox v0.2.0/go.mod h1:td/OI3xSEbMl+gW3b84QESdR20uD3KHHsma59ke7mmQ= +github.com/webitel/crypto/cryptostore v0.2.1 h1:BpbJW1nzdul7+3EBA4TwcIdGrkn1NiS/unF6rN4v3KM= +github.com/webitel/crypto/cryptostore v0.2.1/go.mod h1:Z50z/4exwV376g46NMpCwO+iFjoNkPxS/NV9aiFTTCU= +github.com/webitel/crypto/encoding/jsonc v0.1.0 h1:Utaem86xUXv6y3UeHd0thFCxj+PuG4Wku4vE1SHenzE= +github.com/webitel/crypto/encoding/jsonc v0.1.0/go.mod h1:i1PWHoKplqTsQpN3LK+qzyeGKmwd0HIVtHwaUWE2OZc= +github.com/webitel/crypto/env v0.1.0 h1:bIWQgAwPcThnuX6gOQmWUubVTZrnnavukya8dSXZwRc= +github.com/webitel/crypto/env v0.1.0/go.mod h1:/jyzvmqAY6KGRwiV4oCWaBAeZOW51j1JdgswUnIPxqo= +github.com/webitel/engine/pkg/presign v0.0.0-20260814110632-b0085804d1f0 h1:vhsNIqsMxMafGV9911FbyqfwWtoAAKo8pSG8/Vf+e74= +github.com/webitel/engine/pkg/presign v0.0.0-20260814110632-b0085804d1f0/go.mod h1:I+AXpm+1awxZwjw2pz950F7KndfcgrxOfwK8eXCGRXg= github.com/webitel/engine/pkg/werror v0.0.0-20250508121332-6ae1563235d8 h1:3++AqBWhwSUuhOSNcrSOpW8UdFaUn/CsXm++zzichCI= github.com/webitel/engine/pkg/werror v0.0.0-20250508121332-6ae1563235d8/go.mod h1:xLS6bkOYzvYv0dYXUkd5yvYOtujUrXf9lS0gd4qAGO4= github.com/webitel/webitel-go-kit v0.0.13-0.20240908192731-3abe573c0e41 h1:vj6qE8RtTyz8B4syfUDCkZqULLJ/4I+LS0Rw5W7mao0= @@ -410,8 +418,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= @@ -435,8 +443,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= @@ -446,8 +454,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -475,8 +483,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -484,8 +492,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/model/email_profile.go b/model/email_profile.go index db991ab1..42b56692 100644 --- a/model/email_profile.go +++ b/model/email_profile.go @@ -2,8 +2,9 @@ package model import ( "encoding/json" - "golang.org/x/oauth2" "strings" + + "golang.org/x/oauth2" ) const ( @@ -33,7 +34,7 @@ type EmailProfile struct { Schema Lookup `json:"schema" db:"schema"` Enabled bool `json:"enabled" db:"enabled"` Login string `json:"login" db:"login"` - Password string `json:"password" db:"password"` + Password UserPassword `json:"password" db:"password"` Mailbox string `json:"mailbox" db:"mailbox"` SmtpHost string `json:"smtp_host" db:"smtp_host"` SmtpPort int `json:"smtp_port" db:"smtp_port"` @@ -99,6 +100,9 @@ func (p *EmailProfile) Oauth() (oauth2.Config, AppError) { return oauth2.Config{}, NewBadRequestError("email.profile.valid.oauth", "Not found oauth config") } +// Email account password +type UserPassword = string + type EmailProfileLogin struct { AuthType string `json:"auth_type" db:"auth_type"` RedirectUrl string `json:"redirect_url" db:"redirect_url"` diff --git a/store/sqlstore/crypto_schema.go b/store/sqlstore/crypto_schema.go new file mode 100644 index 00000000..283e0795 --- /dev/null +++ b/store/sqlstore/crypto_schema.go @@ -0,0 +1,75 @@ +package sqlstore + +import ( + "fmt" + "sync" + + "github.com/webitel/crypto/cryptostore/schema" +) + +// CryptoInit configures cryptostore/schema.Codec plugin from environment +func CryptoInit() error { + crypto.init.Do(cryptoInit) + return crypto.err +} + +// guard: crypto.init.(sync.Once) +func cryptoInit() { + // cryptostore/schema.(BASELINE) + schema.Register(&crypto.schema) + // plugin: module environment configuration + crypto.codec, crypto.err = schema.NewCodec( + schema.DefaultOptions(), + ) + if crypto.err == nil { + // schema MAY define field(s) with the "search" option enabled + // this require WBTL_CRYPTO_SEARCH_{KERING|KEYFILE} to be specified + crypto.err = crypto.codec.RequireIndex() + } + if crypto.err != nil { + // wrap up general error details + crypto.err = fmt.Errorf("crypto: configuration ; %w", crypto.err) + } +} + +// Crypto schema.Codec for data encryption +func Crypto() *schema.Codec { + err := CryptoInit() // lazy: init + if err != nil { + panic(err) // configuration failed + } + return crypto.codec +} + +const ( + schemaTableEmailAccount = "call_center.cc_email_profile" +) + +// module: cryptostore/schema +var crypto = struct { + // baseline (mandatory) schema + schema schema.Config + codec *schema.Codec + init sync.Once + err error +} { + + schema: schema.Config{ + Version: 1, + Units: map[string]*schema.Unit{ + schemaTableEmailAccount: { + Fields: map[string]*schema.FieldPolicy{ + "password": {}, + "params": {Nested: []schema.FieldNested{ + {Path: []string{"$.oauth2.client_secret"}}, + }}, + "token": {Nested: []schema.FieldNested{ + {Path: []string{"access_token"}}, + {Path: []string{"refresh_token"}}, + }}, + }, + }, + }, + }, + +} diff --git a/store/sqlstore/crypto_types.go b/store/sqlstore/crypto_types.go new file mode 100644 index 00000000..503cac58 --- /dev/null +++ b/store/sqlstore/crypto_types.go @@ -0,0 +1,246 @@ +package sqlstore + +import ( + "bytes" + "context" + "database/sql" + drv "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + + "github.com/webitel/crypto/cryptostore/schema" +) + +// encryptBytes quietly encrypts given [plain] data value. +// Optional ctx.. MAY describe <[schema.]table> [...] of the data field. +func encryptBytes(plain []byte, tableColumn ...string) []byte { + if len(plain) == 0 { + return nil // , nil // NULL + } + blob, err := Crypto().Base().Encrypt( + context.Background(), plain, + ) + if err != nil { + // failed to encrypt sensitive data + // will retry on next time update .. + args := []any{"err", err} + path := strings.Join(tableColumn, ".") + if path = strings.Trim(path, "."); path != "" { + args = append(args, "column", path) + } + slog.Warn( + "Failed to encrypt sensitive data ; Keeping it plain until next data update", + args..., + ) + blob = []byte(plain) + // return plain, err + } + return blob // , nil +} + +func decryptBytes(into *[]byte) sql.Scanner { + return scanFunc(func(src any) (err error) { + // sanitize: NULL + (*into) = nil + // ciphertext + var blob []byte + err = scanBytes(&blob)(src) + if err != nil || len(blob) == 0 { + return err // NULL -or- ERROR + } + // DECRYPT (optional) + (*into), _, err = Crypto().Base().Decrypt( + context.Background(), blob, + ) + if err != nil { + // failed to decrypt data + (*into) = blob // ciphertext + // err = fmt.Errorf("cryptostore/field: decrypt %s.%s row(id:%s): %w", "schema.table", "column", row.ID, err) + // return err + return errors.New("store.sql.convert_cipher_text") + } + // OK + // (*into) == plaintext + return nil + }) +} + +func encryptText(plain string, tableColumn ...string) []byte { + if plain == "" { + return nil // NULL + } + return encryptBytes([]byte(plain), tableColumn...) +} + +func decryptText(into *string) sql.Scanner { + return scanFunc(func(src any) (err error) { + // sanitize: NULL + (*into) = "" + // ciphertext + var text []byte + err = decryptBytes(&text).Scan(src) + if err != nil || len(text) == 0 { + return err // NULL -or- ERROR + } + // OK + (*into) = string(text) + return nil + }) +} + +var ( + jsonNull = []byte("null") + jsonZeroArray = []byte("[]") + jsonZeroObject = []byte("{}") +) + +// encrypts JSON column nested field(s) according to cryptostore/schema.Unit(table) declaration +func encryptJSONSchema(src any, table, column string) drv.Valuer { + return valueFunc(func() (drv.Value, error) { + + if src == nil { + return nil, nil + } + + data, err := json.Marshal(src) + if err != nil { + return nil, err + } + + for _, none := range [][]byte{ + jsonZeroObject, + jsonZeroArray, + jsonNull, + } { + if bytes.EqualFold(data, none) { + data = nil + break + } + } + + record := schema.Record{ + column: data, + } + crypto := Crypto().Unit(table) + err = crypto.EncodeRecords( + context.Background(), []schema.Record{record}, + ) + if err != nil { + // failed to encrypt + return nil, fmt.Errorf("cryptostore: encrypt %s.%s ; %w", table, column, err) + } + data = record[column].([]byte) + return json.RawMessage(data), nil + }) +} + +// decrypts JSON column nested field(s) according to cryptostore/schema.Unit(table) declaration +func decryptJSONSchema(dst any, table, column string) sql.Scanner { + return scanFunc(func(src any) (err error) { + + var data []byte + err = scanBytes(&data).Scan(src) + if err != nil { + return err + } + + if len(data) == 0 { + return nil // NULL + } + + record := schema.Record{ + column: data, + } + crypto := Crypto().Unit(table) + err = crypto.DecodeRecords( + context.Background(), []schema.Record{record}, + ) + if err != nil { + // failed to decrypt + return fmt.Errorf("cryptostore: decrypt %s.%s ; %w", table, column, err) + } + + data = record[column].([]byte) + err = json.Unmarshal(data, dst) + if err != nil { + return fmt.Errorf("sql: convert JSON into %T", dst) + } + + return nil + }) +} + +// examinate JSON value structure and variadically decrypts encrypted value(s).. +func decryptJSON(dst any) sql.Scanner { + return scanFunc(func(src any) (err error) { + + var data []byte + err = scanBytes(&data).Scan(src) + if err != nil { + return err + } + + if len(data) == 0 { + return nil // NULL + } + + // 1. walk down thru JSON value.(type) + // 2. if string("cbox:") - try to decrypt ! + data, err = schema.DecryptJSONB( + Crypto().Base(), data, + ) + if err != nil { + return fmt.Errorf("cryptostore: decrypt %T ; %w", dst, err) + } + + err = json.Unmarshal(data, dst) + if err != nil { + return fmt.Errorf("sql: convert JSON into %T", dst) + } + + return nil + }) +} + +type scanFunc func(src any) error + +var _ sql.Scanner = scanFunc(nil) + +func (scan scanFunc) Scan(src any) error { + if scan == nil { + return nil + } + return scan(src) +} + +func scanBytes(dst *[]byte) scanFunc { + return func(src any) error { + *dst = nil // DEFAULT NULL + if src == nil { + return nil // NULL + } + switch data := src.(type) { + case []byte: + if data == nil { + return nil // NULL + } + *dst = append(*dst, data...) // copy + default: + return errors.New("store.sql.convert_bytes") + // return errors.Errorf("[database]: convert %[1]T value '%[1]v' to []byte", src) + } + return nil + } +} + +type valueFunc func() (drv.Value, error) + +var _ drv.Valuer = valueFunc(nil) + +// Value implements database/sql/driver.Valuer interface +func (eval valueFunc) Value() (drv.Value, error) { + return eval() +} \ No newline at end of file diff --git a/store/sqlstore/email_profile_store.go b/store/sqlstore/email_profile_store.go index a5d0480b..a7c362db 100644 --- a/store/sqlstore/email_profile_store.go +++ b/store/sqlstore/email_profile_store.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "golang.org/x/oauth2" "github.com/webitel/engine/model" @@ -68,12 +69,12 @@ FROM t "Imap": p.ImapPort, "Smtp": p.SmtpPort, "Login": p.Login, - "Pass": p.Password, + "Pass": encryptText(p.Password, schemaTableEmailAccount, "password"), "CreatedBy": p.CreatedBy.GetSafeId(), "UpdatedBy": p.UpdatedBy.GetSafeId(), "AuthType": p.AuthType, "Listen": p.Listen, - "Params": p.Params.Json(), + "Params": encryptJSONSchema(json.RawMessage(p.Params.Json()), schemaTableEmailAccount, "params"), }) if err != nil { @@ -125,7 +126,7 @@ func (s SqlEmailProfileStore) Get(ctx context.Context, domainId int64, id int) ( t.description, t.enabled, t.password, - t.auth_type, + t.auth_type, t.listen, t.params, t.token->>'expiry' notnull and t.token->>'access_token' notnull as logged @@ -148,26 +149,26 @@ func (s SqlEmailProfileStore) Get(ctx context.Context, domainId int64, id int) ( func (s SqlEmailProfileStore) Update(ctx context.Context, domainId int64, p *model.EmailProfile) (*model.EmailProfile, model.AppError) { var profile *model.EmailProfile err := s.GetMaster().WithContext(ctx).SelectOne(&profile, `with t as ( - update call_center.cc_email_profile - set name = :Name, + update call_center.cc_email_profile set + name = :Name, description= :Description, flow_id = :FlowId, - imap_host = :ImapHost, - login = :Login, - password = :Pass, - mailbox = :Mailbox, - smtp_port = :Smtp, - imap_port = :Imap, - enabled = :Enabled, - updated_by = :UpdatedBy, - updated_at = now(), + imap_host = :ImapHost, + login = :Login, + password = :Pass, + mailbox = :Mailbox, + smtp_port = :Smtp, + imap_port = :Imap, + enabled = :Enabled, + updated_by = :UpdatedBy, + updated_at = now(), smtp_host = :SmtpHost, fetch_interval = :FetchInterval, - auth_type = :AuthType, + auth_type = :AuthType, "listen" = :Listen, - params = case when not :Params::jsonb isnull then :Params::jsonb end - where id = :Id and domain_id = :DomainId - returning * + params = case when not :Params::jsonb isnull then :Params::jsonb end + where id = :Id and domain_id = :DomainId + returning * ) SELECT t.id, t.domain_id, @@ -204,7 +205,7 @@ FROM t "FlowId": p.Schema.Id, "ImapHost": p.ImapHost, "Login": p.Login, - "Pass": p.Password, + "Pass": encryptText(p.Password, schemaTableEmailAccount, "password"), "Mailbox": p.Mailbox, "Smtp": p.SmtpPort, "Imap": p.ImapPort, @@ -215,7 +216,7 @@ FROM t "FetchInterval": p.FetchInterval, "AuthType": p.AuthType, "Listen": p.Listen, - "Params": p.Params.Json(), + "Params": encryptJSONSchema(json.RawMessage(p.Params.Json()), schemaTableEmailAccount, "params"), }) if err != nil { @@ -226,7 +227,7 @@ FROM t } func (s SqlEmailProfileStore) Delete(ctx context.Context, domainId int64, id int) model.AppError { - if _, err := s.GetMaster().WithContext(ctx).Exec(`delete from call_center.cc_email_profile c where c.id=:Id and c.domain_id = :DomainId`, + if _, err := s.GetMaster().WithContext(ctx).Exec(`delete from call_center.cc_email_profile_rc c where c.id=:Id and c.domain_id = :DomainId`, map[string]interface{}{"Id": id, "DomainId": domainId}); err != nil { return model.NewCustomCodeError("store.sql_email_profile.delete.app_error", fmt.Sprintf("Id=%v, %s", id, err.Error()), extractCodeFromErr(err)) } @@ -235,9 +236,14 @@ func (s SqlEmailProfileStore) Delete(ctx context.Context, domainId int64, id int func (s SqlEmailProfileStore) SetupOAuth2(ctx context.Context, id int, token *oauth2.Token) model.AppError { - data, _ := json.Marshal(token) + // data, _ := json.Marshal(token) + data, err := encryptJSONSchema(token, schemaTableEmailAccount, "token").Value() + if err != nil { + // failed to encrypt sensitive data + return model.NewCustomCodeError("store.sql_email_profile.oauth.app_error", fmt.Sprintf("Id=%v, %s", id, err.Error()), extractCodeFromErr(err)) + } - _, err := s.GetMaster().WithContext(ctx).Exec(`update call_center.cc_email_profile + _, err = s.GetMaster().WithContext(ctx).Exec(`update call_center.cc_email_profile set token = :Token where id = :Id;`, map[string]interface{}{ "Id": id, diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 23b6a423..1f508ce9 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -517,9 +517,7 @@ func (me typeConverter) FromDb(target any) (gorp.CustomScanner, bool) { *model.EavesdropInfo, *model.Questions, *model.QuestionAnswers, - **model.MailProfileParams, *[]*model.BlindTransfer, - *model.MailProfileParams, *[]*model.CallForm, **model.QualityMetrics: binder := func(holder, target any) error { @@ -636,6 +634,26 @@ func (me typeConverter) FromDb(target any) (gorp.CustomScanner, bool) { } } return gorp.CustomScanner{Holder: new([]byte), Target: target, Binder: binder}, true + // ----- ENCRYPTED ----- // + case *model.UserPassword: + { + cast := func(src, dst any) (err error) { + data := src.(*[]byte) + into := dst.(*model.UserPassword) + return decryptText(into).Scan(*data) + } + return gorp.CustomScanner{Holder: new([]byte), Target: target, Binder: cast}, true + } + case *model.MailProfileParams, + **model.MailProfileParams: + { + cast := func(src, dst any) (err error) { + data := src.(*[]byte) + into := dst // .(*model.MailProfileParams) + return decryptJSON(into).Scan(*data) + } + return gorp.CustomScanner{Holder: new([]byte), Target: target, Binder: cast}, true + } } return gorp.CustomScanner{}, false