fix: align customer payment encryption and protect original values
Deploy olons-admin production / deploy (push) Successful in 9m19s
Deploy olons-admin production / deploy (push) Successful in 9m19s
This commit is contained in:
@@ -458,6 +458,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
if row.HolderCipher != "" {
|
||||
plain, decryptErr := encrypt.DecryptField(ctx, row.HolderCipher)
|
||||
if decryptErr != nil {
|
||||
row.SensitiveUnavailable = true
|
||||
g.Log().Warningf(ctx, "收款人姓名解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
|
||||
row.Holder = row.HolderMask
|
||||
} else {
|
||||
@@ -469,6 +470,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
if row.IdentityNoCipher != "" {
|
||||
plain, decryptErr := encrypt.DecryptField(ctx, row.IdentityNoCipher)
|
||||
if decryptErr != nil {
|
||||
row.SensitiveUnavailable = true
|
||||
g.Log().Warningf(ctx, "收款人证件号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
|
||||
row.IdentityNo = row.IdentityNoMask
|
||||
} else {
|
||||
@@ -480,6 +482,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
if row.MobileCipher != "" {
|
||||
plain, decryptErr := encrypt.DecryptField(ctx, row.MobileCipher)
|
||||
if decryptErr != nil {
|
||||
row.SensitiveUnavailable = true
|
||||
g.Log().Warningf(ctx, "收款手机号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
|
||||
row.Mobile = row.MobileMask
|
||||
} else {
|
||||
@@ -491,6 +494,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
if row.AccountNoCipher != "" {
|
||||
plain, decryptErr := encrypt.DecryptField(ctx, row.AccountNoCipher)
|
||||
if decryptErr != nil {
|
||||
row.SensitiveUnavailable = true
|
||||
g.Log().Warningf(ctx, "收款账号解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
|
||||
row.AccountNo = row.AccountNoMask
|
||||
} else {
|
||||
@@ -502,6 +506,7 @@ func (s *sSysAppMember) PaymentList(ctx context.Context, in *sysin.AppMemberPaym
|
||||
if row.DigitalRmbCodeCipher != "" {
|
||||
plain, decryptErr := encrypt.DecryptField(ctx, row.DigitalRmbCodeCipher)
|
||||
if decryptErr != nil {
|
||||
row.SensitiveUnavailable = true
|
||||
g.Log().Warningf(ctx, "数字钱包编码解密失败,回退为脱敏值 memberId:%d accountId:%d err:%+v", row.MemberId, row.Id, decryptErr)
|
||||
row.DigitalRMBCode = row.DigitalRMBCodeMask
|
||||
} else {
|
||||
@@ -534,7 +539,7 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.AlipayQrAttachmentId] = in.AlipayQrAttachmentId
|
||||
}
|
||||
if in.Holder != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.Holder)
|
||||
v, e := encrypt.EncryptPaymentField(ctx, in.Holder)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -542,7 +547,7 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.HolderMask] = encrypt.MaskName(in.Holder)
|
||||
}
|
||||
if in.Mobile != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.Mobile)
|
||||
v, e := encrypt.EncryptPaymentField(ctx, in.Mobile)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -550,11 +555,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.MobileMask] = encrypt.MaskMobile(in.Mobile)
|
||||
}
|
||||
if in.IdentityNo != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.IdentityNo)
|
||||
v, e := encrypt.EncryptPaymentField(ctx, in.IdentityNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
h, e := encrypt.HashField(ctx, in.IdentityNo)
|
||||
h, e := encrypt.HashPaymentField(ctx, in.IdentityNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -563,11 +568,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.IdentityNoMask] = encrypt.MaskAccount(in.IdentityNo)
|
||||
}
|
||||
if in.AccountNo != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.AccountNo)
|
||||
v, e := encrypt.EncryptPaymentField(ctx, in.AccountNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
h, e := encrypt.HashField(ctx, in.AccountNo)
|
||||
h, e := encrypt.HashPaymentField(ctx, in.AccountNo)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -576,11 +581,11 @@ func (s *sSysAppMember) PaymentEdit(ctx context.Context, in *sysin.AppMemberPaym
|
||||
data[c.AccountNoMask] = encrypt.MaskAccount(in.AccountNo)
|
||||
}
|
||||
if in.DigitalRMBCode != "" {
|
||||
v, e := encrypt.EncryptField(ctx, in.DigitalRMBCode)
|
||||
v, e := encrypt.EncryptPaymentField(ctx, in.DigitalRMBCode)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
h, e := encrypt.HashField(ctx, in.DigitalRMBCode)
|
||||
h, e := encrypt.HashPaymentField(ctx, in.DigitalRMBCode)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -95,6 +95,9 @@ type AppMemberEditInp struct {
|
||||
}
|
||||
|
||||
func (in *AppMemberEditInp) Filter(ctx context.Context) error {
|
||||
if err := rejectMaskedSensitiveFields(in.RealName, in.IdNo); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Id == 0 {
|
||||
return gerror.New("后台不支持创建会员,请由会员在C端完成注册")
|
||||
}
|
||||
@@ -156,6 +159,7 @@ type AppMemberPaymentListInp struct {
|
||||
func (in *AppMemberPaymentListInp) Filter(ctx context.Context) error { return nil }
|
||||
|
||||
type AppMemberPaymentModel struct {
|
||||
SensitiveUnavailable bool `json:"sensitiveUnavailable"`
|
||||
Id uint64 `json:"id"`
|
||||
MemberId uint64 `json:"memberId"`
|
||||
AccountType int `json:"accountType"`
|
||||
@@ -207,6 +211,9 @@ type AppMemberPaymentEditInp struct {
|
||||
}
|
||||
|
||||
func (in *AppMemberPaymentEditInp) Filter(ctx context.Context) error {
|
||||
if err := rejectMaskedSensitiveFields(in.Holder, in.IdentityNo, in.Mobile, in.AccountNo, in.DigitalRMBCode); err != nil {
|
||||
return err
|
||||
}
|
||||
if !dict.HasOptionKey(consts.PaymentAccountTypeOptions, in.AccountType) {
|
||||
return gerror.New("收款账户类型不合法")
|
||||
}
|
||||
@@ -260,12 +267,24 @@ type AppMemberAddressEditInp struct {
|
||||
}
|
||||
|
||||
func (in *AppMemberAddressEditInp) Filter(ctx context.Context) error {
|
||||
if err := rejectMaskedSensitiveFields(in.Receiver, in.Mobile, in.Address); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Id == 0 && (strings.TrimSpace(in.Receiver) == "" || strings.TrimSpace(in.Mobile) == "" || strings.TrimSpace(in.Address) == "") {
|
||||
return gerror.New("新增地址时收货人、手机号和详细地址不能为空")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rejectMaskedSensitiveFields(values ...string) error {
|
||||
for _, value := range values {
|
||||
if strings.Contains(value, "*") {
|
||||
return gerror.New("不能保存带星号的脱敏资料,请填写完整信息或留空保留原值")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AppMemberAddressDeleteInp struct {
|
||||
Id uint64 `json:"id" v:"required|min:1#地址ID不能为空|地址ID不能为空"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
func TestPaymentCipherFieldsScanWithoutJSONExposure(t *testing.T) {
|
||||
const ciphertext = "test-ciphertext-not-a-real-bank-account"
|
||||
record := gdb.Record{}
|
||||
for _, field := range []string{"holder_cipher", "identity_no_cipher", "mobile_cipher", "account_no_cipher", "digital_rmb_code_cipher"} {
|
||||
record[field] = gvar.New(ciphertext)
|
||||
}
|
||||
var row AppMemberPaymentModel
|
||||
if err := record.Struct(&row); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, actual := range map[string]string{"holder": row.HolderCipher, "identity": row.IdentityNoCipher, "mobile": row.MobileCipher, "account": row.AccountNoCipher, "digital": row.DigitalRmbCodeCipher} {
|
||||
if actual != ciphertext {
|
||||
t.Errorf("%s ciphertext was not loaded by database scan", name)
|
||||
}
|
||||
}
|
||||
data, err := json.Marshal(row)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), ciphertext) {
|
||||
t.Fatal("internal ciphertext must not be exposed in API JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentEditRejectsMaskedValues(t *testing.T) {
|
||||
for _, field := range []string{"holder", "account", "identity", "mobile", "digital"} {
|
||||
t.Run(field, func(t *testing.T) {
|
||||
in := &AppMemberPaymentEditInp{Id: 1, MemberId: 1, AccountType: 1}
|
||||
switch field {
|
||||
case "holder":
|
||||
in.Holder = "王*"
|
||||
case "account":
|
||||
in.AccountNo = "6222********5937"
|
||||
case "identity":
|
||||
in.IdentityNo = "1234********5678"
|
||||
case "mobile":
|
||||
in.Mobile = "138****1234"
|
||||
case "digital":
|
||||
in.DigitalRMBCode = "0021********8708"
|
||||
}
|
||||
if err := in.Filter(context.Background()); err == nil {
|
||||
t.Fatal("masked values must never overwrite originals")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,11 @@ token:
|
||||
# 业务敏感字段保护。生产环境必须替换为独立的高强度密钥,并通过密钥管理系统注入。
|
||||
security:
|
||||
dataKey: "change-me-before-production"
|
||||
# 与 C 端收款资料使用相同配置;历史主密钥只用于读取,不修改原密文。
|
||||
financeMasterKey: ""
|
||||
financeLegacyMasterKeys: []
|
||||
financeDataEncryptionKey: ""
|
||||
financeLookupHmacKey: ""
|
||||
|
||||
|
||||
# 消息队列
|
||||
|
||||
@@ -54,30 +54,32 @@ func DecryptField(ctx context.Context, encoded string) (string, error) {
|
||||
if encoded == "" {
|
||||
return "", nil
|
||||
}
|
||||
key, err := fieldKey(ctx)
|
||||
raw, err := decodeFieldPayload(encoded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "敏感字段密文格式错误")
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(raw) < gcm.NonceSize() {
|
||||
if len(raw) < 12+16 {
|
||||
return "", gerror.New("敏感字段密文不完整")
|
||||
}
|
||||
plain, err := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "敏感字段解密失败")
|
||||
keys := financeFieldReadKeys(ctx)
|
||||
if key, keyErr := fieldKey(ctx); keyErr == nil {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return string(plain), nil
|
||||
for _, key := range keys {
|
||||
block, keyErr := aes.NewCipher(key)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
gcm, keyErr := cipher.NewGCM(block)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
plain, decryptErr := gcm.Open(nil, raw[:gcm.NonceSize()], raw[gcm.NonceSize():], nil)
|
||||
if decryptErr == nil {
|
||||
return string(plain), nil
|
||||
}
|
||||
}
|
||||
return "", gerror.New("敏感字段解密失败,请核对当前及历史密钥配置")
|
||||
}
|
||||
|
||||
// HashField creates a deterministic, non-reversible lookup key with a secret pepper.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// financeFieldKey uses the same key derivation as the customer-facing finance service.
|
||||
// An empty configuration retains the existing HotGo format for older installations.
|
||||
func financeFieldKey(ctx context.Context, purpose string) ([]byte, error) {
|
||||
master := strings.TrimSpace(g.Cfg().MustGet(ctx, "security.financeMasterKey", "").String())
|
||||
if master != "" {
|
||||
if len([]byte(master)) < 32 {
|
||||
return nil, gerror.New("收款资料密钥配置不正确")
|
||||
}
|
||||
return deriveFinanceFieldKey(master, purpose), nil
|
||||
}
|
||||
name := "security.financeDataEncryptionKey"
|
||||
if purpose == "lookup-hmac" {
|
||||
name = "security.financeLookupHmacKey"
|
||||
}
|
||||
encoded := strings.TrimSpace(g.Cfg().MustGet(ctx, name, "").String())
|
||||
if encoded == "" {
|
||||
return nil, nil
|
||||
}
|
||||
key, err := decodeFieldPayload(encoded)
|
||||
if err != nil || (purpose == "data-encryption" && len(key) != 32) || (purpose == "lookup-hmac" && len(key) < 32) {
|
||||
return nil, gerror.New("收款资料密钥配置不正确")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func deriveFinanceFieldKey(master, purpose string) []byte {
|
||||
mac := hmac.New(sha256.New, []byte(master))
|
||||
_, _ = mac.Write([]byte("olons-finance:" + purpose + ":v1"))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func financeFieldReadKeys(ctx context.Context) [][]byte {
|
||||
keys := make([][]byte, 0)
|
||||
if key, err := financeFieldKey(ctx, "data-encryption"); err == nil && len(key) > 0 {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for _, master := range g.Cfg().MustGet(ctx, "security.financeLegacyMasterKeys", []string{}).Strings() {
|
||||
if master = strings.TrimSpace(master); len([]byte(master)) >= 32 {
|
||||
keys = append(keys, deriveFinanceFieldKey(master, "data-encryption"))
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func decodeFieldPayload(value string) ([]byte, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
for _, encoding := range []*base64.Encoding{base64.RawURLEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.StdEncoding} {
|
||||
if raw, err := encoding.DecodeString(value); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
return nil, gerror.New("敏感字段密文格式错误")
|
||||
}
|
||||
|
||||
// EncryptPaymentField keeps new admin edits readable by the customer-facing service.
|
||||
func EncryptPaymentField(ctx context.Context, plaintext string) (string, error) {
|
||||
if plaintext == "" {
|
||||
return "", nil
|
||||
}
|
||||
key, err := financeFieldKey(ctx, "data-encryption")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return EncryptField(ctx, plaintext)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "初始化收款资料加密失败")
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "初始化收款资料加密失败")
|
||||
}
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", gerror.Wrap(err, "生成收款资料随机数失败")
|
||||
}
|
||||
return base64.RawStdEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(plaintext), nil)), nil
|
||||
}
|
||||
|
||||
func HashPaymentField(ctx context.Context, value string) (string, error) {
|
||||
key, err := financeFieldKey(ctx, "lookup-hmac")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(key) == 0 {
|
||||
return HashField(ctx, value)
|
||||
}
|
||||
mac := hmac.New(sha256.New, key)
|
||||
_, _ = mac.Write([]byte(strings.TrimSpace(value)))
|
||||
return hex.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
)
|
||||
|
||||
func TestDecryptFieldAcceptsCustomerPaymentKeys(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
current := "current-finance-master-key-with-at-least-32-characters"
|
||||
previous := "previous-finance-master-key-with-at-least-32-characters"
|
||||
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": current, "financeLegacyMasterKeys": []string{previous}})
|
||||
for _, master := range []string{current, previous} {
|
||||
encoded := customerPaymentFixture(t, master, "6222000000001234")
|
||||
plain, err := DecryptField(ctx, encoded)
|
||||
if err != nil || plain != "6222000000001234" {
|
||||
t.Fatalf("customer payment must decrypt with current or previous key: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentWritesRemainReadableByCustomerService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
master := "current-finance-master-key-with-at-least-32-characters"
|
||||
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": master})
|
||||
const value = "6222000000001234"
|
||||
encoded, err := EncryptPaymentField(ctx, value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := base64.RawStdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
t.Fatal("payment edit must use the customer's standard Base64 format")
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(master))
|
||||
_, _ = mac.Write([]byte("olons-finance:data-encryption:v1"))
|
||||
block, _ := aes.NewCipher(mac.Sum(nil))
|
||||
aead, _ := cipher.NewGCM(block)
|
||||
plain, err := aead.Open(nil, payload[:aead.NonceSize()], payload[aead.NonceSize():], nil)
|
||||
if err != nil || string(plain) != value {
|
||||
t.Fatalf("customer service cannot read admin edit: %v", err)
|
||||
}
|
||||
actualHash, err := HashPaymentField(ctx, value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mac = hmac.New(sha256.New, []byte(master))
|
||||
_, _ = mac.Write([]byte("olons-finance:lookup-hmac:v1"))
|
||||
lookup := hmac.New(sha256.New, mac.Sum(nil))
|
||||
_, _ = lookup.Write([]byte(value))
|
||||
if actualHash != hex.EncodeToString(lookup.Sum(nil)) {
|
||||
t.Fatal("admin lookup hash must match customer service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptFieldKeepsLegacyHotGoDataReadable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
setFieldTestConfig(t, map[string]interface{}{"dataKey": "admin-legacy-key", "financeMasterKey": "current-finance-master-key-with-at-least-32-characters"})
|
||||
encoded, err := EncryptField(ctx, "旧资料")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plain, err := DecryptField(ctx, encoded)
|
||||
if err != nil || plain != "旧资料" {
|
||||
t.Fatalf("legacy HotGo record unreadable: %v", err)
|
||||
}
|
||||
for _, invalid := range []string{"DEMO-NOT-CIPHERTEXT", "AAAA", customerPaymentFixture(t, "unknown-master-key-with-at-least-32-characters", "姓名")} {
|
||||
if _, err := DecryptField(ctx, invalid); err == nil {
|
||||
t.Fatal("invalid or unknown-key records must not be reported as plaintext")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setFieldTestConfig(t *testing.T, security map[string]interface{}) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(map[string]interface{}{"security": security})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
adapter, err := gcfg.NewAdapterContent(string(data))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := g.Cfg().GetAdapter()
|
||||
g.Cfg().SetAdapter(adapter)
|
||||
t.Cleanup(func() { g.Cfg().SetAdapter(previous) })
|
||||
}
|
||||
|
||||
func customerPaymentFixture(t *testing.T, master, value string) string {
|
||||
t.Helper()
|
||||
mac := hmac.New(sha256.New, []byte(master))
|
||||
_, _ = mac.Write([]byte("olons-finance:data-encryption:v1"))
|
||||
block, err := aes.NewCipher(mac.Sum(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nonce := []byte{251, 255, 239, 255, 251, 255, 239, 255, 251, 255, 239, 255}
|
||||
return base64.RawStdEncoding.EncodeToString(aead.Seal(nonce, nonce, []byte(value), nil))
|
||||
}
|
||||
@@ -178,8 +178,10 @@
|
||||
<n-spin :show="relatedLoading">
|
||||
<n-form label-placement="left" label-width="110">
|
||||
<template v-if="relatedType === 'payment'">
|
||||
<n-alert v-if="paymentForm.id" type="warning" :show-icon="false">
|
||||
敏感资料已明文展示;请仅用于当前业务处理。留空保存不会覆盖原敏感字段或收款码。
|
||||
<n-alert v-if="paymentForm.id" :type="paymentForm.sensitiveUnavailable ? 'error' : 'warning'" :show-icon="false">
|
||||
{{ paymentForm.sensitiveUnavailable
|
||||
? '部分历史资料暂无法解密。脱敏值只作占位,不会写回原数据;请留空保留原值。'
|
||||
: '资料已明文展示。留空保存不会覆盖原字段或收款码。' }}
|
||||
</n-alert>
|
||||
<n-form-item label="开户人"
|
||||
><n-input v-model:value="paymentForm.holder" :placeholder="paymentForm.holderMask || '留空不修改'"
|
||||
@@ -734,16 +736,17 @@
|
||||
reset(paymentForm, {
|
||||
memberId,
|
||||
id: bank?.id || 0,
|
||||
holder: bank?.holder || bank?.holderMask || '',
|
||||
holder: editableSensitive(bank?.holder),
|
||||
sensitiveUnavailable: bank?.sensitiveUnavailable || false,
|
||||
holderMask: bank?.holderMask || '',
|
||||
identityNo: bank?.identityNo || bank?.identityNoMask || '',
|
||||
identityNo: editableSensitive(bank?.identityNo),
|
||||
identityNoMask: bank?.identityNoMask || '',
|
||||
bankName: bank?.bankName || '',
|
||||
mobile: bank?.mobile || bank?.mobileMask || '',
|
||||
mobile: editableSensitive(bank?.mobile),
|
||||
mobileMask: bank?.mobileMask || '',
|
||||
bankAccount: bank?.accountNo || bank?.accountNoMask || '',
|
||||
bankAccount: editableSensitive(bank?.accountNo),
|
||||
accountNoMask: bank?.accountNoMask || '',
|
||||
digitalWallet: bank?.digitalRmbCode || bank?.digitalRmbCodeMask || '',
|
||||
digitalWallet: editableSensitive(bank?.digitalRmbCode),
|
||||
digitalRmbCodeMask: bank?.digitalRmbCodeMask || '',
|
||||
alipayQrCode: bank?.alipayQrUrl || '',
|
||||
wechatQrCode: bank?.wechatQrUrl || '',
|
||||
@@ -756,14 +759,14 @@
|
||||
reset(addressForm, {
|
||||
id: row?.id || 0,
|
||||
memberId,
|
||||
receiver: row?.receiver || row?.receiverMask || '',
|
||||
mobile: row?.mobile || row?.mobileMask || '',
|
||||
receiver: editableSensitive(row?.receiver),
|
||||
mobile: editableSensitive(row?.mobile),
|
||||
region,
|
||||
originalRegion: region,
|
||||
province: row?.province || '',
|
||||
city: row?.city || '',
|
||||
district: row?.district || '',
|
||||
address: '',
|
||||
address: editableSensitive(row?.address),
|
||||
isDefault: 1,
|
||||
});
|
||||
}
|
||||
@@ -778,6 +781,9 @@
|
||||
: await AddressList({ memberId: detail.value.id });
|
||||
relatedRows.value = r.list || [];
|
||||
}
|
||||
function editableSensitive(value: unknown) {
|
||||
return typeof value === 'string' && !value.includes('*') ? value : '';
|
||||
}
|
||||
function startRelatedAdd() {
|
||||
showRelatedForm.value = true;
|
||||
if (relatedType.value === 'payment')
|
||||
@@ -788,7 +794,8 @@
|
||||
bankName: '',
|
||||
holder: '',
|
||||
mobile: '',
|
||||
accountNo: '',
|
||||
bankAccount: '',
|
||||
digitalWallet: '',
|
||||
verifyStatus: 1,
|
||||
isDefault: 2,
|
||||
});
|
||||
@@ -801,7 +808,7 @@
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: row?.address || row?.addressMask || '',
|
||||
address: '',
|
||||
isDefault: 2,
|
||||
});
|
||||
}
|
||||
@@ -855,11 +862,16 @@
|
||||
memberId: row.memberId,
|
||||
accountType: row.accountType,
|
||||
bankName: row.bankName,
|
||||
holder: row.holder || row.holderMask || '',
|
||||
identityNo: row.identityNo || row.identityNoMask || '',
|
||||
mobile: row.mobile || row.mobileMask || '',
|
||||
accountNo: row.accountNo || row.accountNoMask || '',
|
||||
digitalRmbCode: row.digitalRmbCode || row.digitalRmbCodeMask || '',
|
||||
holder: editableSensitive(row.holder),
|
||||
sensitiveUnavailable: row.sensitiveUnavailable || false,
|
||||
identityNo: editableSensitive(row.identityNo),
|
||||
mobile: editableSensitive(row.mobile),
|
||||
bankAccount: editableSensitive(row.accountNo),
|
||||
digitalWallet: editableSensitive(row.digitalRmbCode),
|
||||
wechatQrCode: row.wechatQrUrl || '',
|
||||
alipayQrCode: row.alipayQrUrl || '',
|
||||
wechatQrAttachmentId: row.wechatQrAttachmentId || 0,
|
||||
alipayQrAttachmentId: row.alipayQrAttachmentId || 0,
|
||||
verifyStatus: row.verifyStatus,
|
||||
isDefault: row.isDefault,
|
||||
});
|
||||
@@ -867,12 +879,12 @@
|
||||
reset(addressForm, {
|
||||
id: row.id,
|
||||
memberId: row.memberId,
|
||||
receiver: row.receiver || row.receiverMask || '',
|
||||
mobile: row.mobile || row.mobileMask || '',
|
||||
receiver: editableSensitive(row.receiver),
|
||||
mobile: editableSensitive(row.mobile),
|
||||
province: row.province,
|
||||
city: row.city,
|
||||
district: row.district,
|
||||
address: row.address || row.addressMask || '',
|
||||
address: editableSensitive(row.address),
|
||||
isDefault: row.isDefault,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user