fix: restore payment account details

This commit is contained in:
richjohnrambo
2026-10-19 11:14:09 +08:00
parent 21371061ed
commit 1c11c0fa82
5 changed files with 126 additions and 86 deletions
+16 -8
View File
@@ -64,14 +64,15 @@ type RedisConfig struct {
} }
type AuthConfig struct { type AuthConfig struct {
APIKeys []string `json:"api_keys"` APIKeys []string `json:"api_keys"`
SessionKeyPattern string `json:"session_key_pattern"` SessionKeyPattern string `json:"session_key_pattern"`
SessionTTLSeconds int `json:"session_ttl_seconds"` SessionTTLSeconds int `json:"session_ttl_seconds"`
DataEncryptionKey string `json:"data_encryption_key"` DataEncryptionKey string `json:"data_encryption_key"`
LookupHMACKey string `json:"lookup_hmac_key"` LookupHMACKey string `json:"lookup_hmac_key"`
FinanceMasterKey string `json:"finance_master_key"` FinanceMasterKey string `json:"finance_master_key"`
AllowDevHeaderAuth bool `json:"allow_dev_header_auth"` FinanceLegacyMasterKeys []string `json:"finance_legacy_master_keys"`
RequiredAPIKey bool `json:"required_api_key"` AllowDevHeaderAuth bool `json:"allow_dev_header_auth"`
RequiredAPIKey bool `json:"required_api_key"`
} }
func Load() (*Config, error) { func Load() (*Config, error) {
@@ -85,7 +86,11 @@ func Load() (*Config, error) {
return nil, err return nil, err
} }
} }
configuredFinanceMasterKey := strings.TrimSpace(cfg.Auth.FinanceMasterKey)
applyEnv(cfg) applyEnv(cfg)
if configuredFinanceMasterKey != "" && configuredFinanceMasterKey != strings.TrimSpace(cfg.Auth.FinanceMasterKey) {
cfg.Auth.FinanceLegacyMasterKeys = append(cfg.Auth.FinanceLegacyMasterKeys, configuredFinanceMasterKey)
}
if err := cfg.validate(); err != nil { if err := cfg.validate(); err != nil {
return nil, err return nil, err
} }
@@ -134,6 +139,9 @@ func applyEnv(cfg *Config) {
setString(&cfg.Auth.DataEncryptionKey, "OLONS_SERVER_DATA_ENCRYPTION_KEY") setString(&cfg.Auth.DataEncryptionKey, "OLONS_SERVER_DATA_ENCRYPTION_KEY")
setString(&cfg.Auth.LookupHMACKey, "OLONS_SERVER_LOOKUP_HMAC_KEY") setString(&cfg.Auth.LookupHMACKey, "OLONS_SERVER_LOOKUP_HMAC_KEY")
setString(&cfg.Auth.FinanceMasterKey, "OLONS_SERVER_FINANCE_MASTER_KEY") setString(&cfg.Auth.FinanceMasterKey, "OLONS_SERVER_FINANCE_MASTER_KEY")
if keys := strings.TrimSpace(os.Getenv("OLONS_SERVER_FINANCE_LEGACY_MASTER_KEYS")); keys != "" {
cfg.Auth.FinanceLegacyMasterKeys = append(cfg.Auth.FinanceLegacyMasterKeys, splitCSV(keys)...)
}
setBool(&cfg.Auth.AllowDevHeaderAuth, "OLONS_SERVER_ALLOW_DEV_HEADER_AUTH") setBool(&cfg.Auth.AllowDevHeaderAuth, "OLONS_SERVER_ALLOW_DEV_HEADER_AUTH")
setBool(&cfg.Auth.RequiredAPIKey, "OLONS_SERVER_REQUIRED_API_KEY") setBool(&cfg.Auth.RequiredAPIKey, "OLONS_SERVER_REQUIRED_API_KEY")
ApplyUploadEnv(&cfg.Upload) ApplyUploadEnv(&cfg.Upload)
+23 -1
View File
@@ -1,6 +1,10 @@
package config package config
import "testing" import (
"os"
"path/filepath"
"testing"
)
func TestDefaultConfigUsesOlonsAdmin(t *testing.T) { func TestDefaultConfigUsesOlonsAdmin(t *testing.T) {
cfg := defaultConfig() cfg := defaultConfig()
@@ -17,6 +21,24 @@ func TestConfigRejectsOtherDatabase(t *testing.T) {
} }
} }
func TestLoadPreservesConfiguredFinanceKeyAsLegacyWhenEnvironmentOverridesIt(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.json")
data := []byte(`{"auth":{"finance_master_key":"legacy-finance-master-key-with-at-least-32-characters"}}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("OLONS_SERVER_CONFIG", path)
t.Setenv("OLONS_SERVER_FINANCE_MASTER_KEY", "current-finance-master-key-with-at-least-32-characters")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if len(cfg.Auth.FinanceLegacyMasterKeys) != 1 ||
cfg.Auth.FinanceLegacyMasterKeys[0] != "legacy-finance-master-key-with-at-least-32-characters" {
t.Fatalf("configured finance key was not retained for historical data: %#v", cfg.Auth.FinanceLegacyMasterKeys)
}
}
func TestApplyUploadSystemConfig(t *testing.T) { func TestApplyUploadSystemConfig(t *testing.T) {
cfg := UploadConfig{Driver: "local", Region: "us-east-1"} cfg := UploadConfig{Driver: "local", Region: "us-east-1"}
ApplyUploadSystemConfig(&cfg, map[string]string{ ApplyUploadSystemConfig(&cfg, map[string]string{
+48 -53
View File
@@ -35,7 +35,6 @@ const (
var ( var (
tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`) tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`)
idempotencyPattern = regexp.MustCompile(`^[A-Za-z0-9._:\-]{8,80}$`) idempotencyPattern = regexp.MustCompile(`^[A-Za-z0-9._:\-]{8,80}$`)
identityNoPattern = regexp.MustCompile(`^[1-9][0-9]{14}(?:[0-9]{2}[0-9X])?$`)
) )
type Repository interface { type Repository interface {
@@ -59,19 +58,19 @@ type Repository interface {
} }
type Service struct { type Service struct {
repository Repository repository Repository
redis *redis.Client redis *redis.Client
aead cipher.AEAD aead cipher.AEAD
lookupKey []byte legacyAEADs []cipher.AEAD
ready bool lookupKey []byte
now func() time.Time ready bool
now func() time.Time
} }
type PaymentAccountInput struct { type PaymentAccountInput struct {
ID int64 `json:"id"` ID int64 `json:"id"`
BankName string `json:"bank_name"` BankName string `json:"bank_name"`
HolderName string `json:"holder_name"` HolderName string `json:"holder_name"`
IdentityNo string `json:"identity_no"`
AccountNo string `json:"account_no"` AccountNo string `json:"account_no"`
DigitalRMBCode string `json:"digital_rmb_code"` DigitalRMBCode string `json:"digital_rmb_code"`
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"` WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
@@ -84,7 +83,6 @@ type PaymentAccount struct {
BankCode string `json:"bank_code"` BankCode string `json:"bank_code"`
BankName string `json:"bank_name"` BankName string `json:"bank_name"`
HolderMask string `json:"holder_mask"` HolderMask string `json:"holder_mask"`
IdentityNoMask string `json:"identity_no_mask"`
AccountNoMask string `json:"account_no_mask"` AccountNoMask string `json:"account_no_mask"`
DigitalRMBCodeMask string `json:"digital_rmb_code_mask"` DigitalRMBCodeMask string `json:"digital_rmb_code_mask"`
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"` WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
@@ -99,7 +97,6 @@ type PaymentAccountDetail struct {
ID int64 `json:"id"` ID int64 `json:"id"`
BankName string `json:"bank_name"` BankName string `json:"bank_name"`
HolderName string `json:"holder_name"` HolderName string `json:"holder_name"`
IdentityNo string `json:"identity_no"`
AccountNo string `json:"account_no"` AccountNo string `json:"account_no"`
DigitalRMBCode string `json:"digital_rmb_code"` DigitalRMBCode string `json:"digital_rmb_code"`
WechatQRURL string `json:"wechat_qr_url"` WechatQRURL string `json:"wechat_qr_url"`
@@ -266,20 +263,39 @@ func New(repository Repository, redisClient *redis.Client, cfg config.AuthConfig
return service return service
} }
} }
block, err := aes.NewCipher(encryptionKey) aead, err := newAEAD(encryptionKey)
if err != nil {
return service
}
aead, err := cipher.NewGCM(block)
if err != nil { if err != nil {
return service return service
} }
service.aead = aead service.aead = aead
seenLegacyKeys := make(map[string]struct{}, len(cfg.FinanceLegacyMasterKeys))
for _, legacyMasterKey := range cfg.FinanceLegacyMasterKeys {
legacyMasterKey = strings.TrimSpace(legacyMasterKey)
if len([]byte(legacyMasterKey)) < 32 || legacyMasterKey == masterKey {
continue
}
if _, exists := seenLegacyKeys[legacyMasterKey]; exists {
continue
}
legacyAEAD, legacyErr := newAEAD(deriveKey(legacyMasterKey, "data-encryption"))
if legacyErr == nil {
service.legacyAEADs = append(service.legacyAEADs, legacyAEAD)
seenLegacyKeys[legacyMasterKey] = struct{}{}
}
}
service.lookupKey = lookupKey service.lookupKey = lookupKey
service.ready = repository != nil && redisClient != nil service.ready = repository != nil && redisClient != nil
return service return service
} }
func newAEAD(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func deriveKey(masterKey, purpose string) []byte { func deriveKey(masterKey, purpose string) []byte {
mac := hmac.New(sha256.New, []byte(masterKey)) mac := hmac.New(sha256.New, []byte(masterKey))
_, _ = mac.Write([]byte("olons-finance:" + purpose + ":v1")) _, _ = mac.Write([]byte("olons-finance:" + purpose + ":v1"))
@@ -312,20 +328,13 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
items := make([]PaymentAccountDetail, 0, len(rows)) items := make([]PaymentAccountDetail, 0, len(rows))
for _, row := range rows { for _, row := range rows {
holderName, accountNo := row.HolderMask, row.AccountNoMask holderName, accountNo := row.HolderMask, row.AccountNoMask
identityNo, digitalRMBCode := row.IdentityNoMask, row.DigitalRMBCodeMask digitalRMBCode := row.DigitalRMBCodeMask
masked := true masked := true
if s.ready { if s.ready {
decryptedHolder, holderErr := s.decrypt(row.HolderCipher) decryptedHolder, holderErr := s.decrypt(row.HolderCipher)
decryptedAccount, accountErr := s.decrypt(row.AccountNoCipher) decryptedAccount, accountErr := s.decrypt(row.AccountNoCipher)
if holderErr == nil && accountErr == nil { if holderErr == nil && accountErr == nil {
holderName, accountNo, masked = decryptedHolder, decryptedAccount, false holderName, accountNo, masked = decryptedHolder, decryptedAccount, false
if row.IdentityNoCipher != "" {
if value, decryptErr := s.decrypt(row.IdentityNoCipher); decryptErr == nil {
identityNo = value
} else {
masked = true
}
}
if row.DigitalRMBCodeCipher != "" { if row.DigitalRMBCodeCipher != "" {
if value, decryptErr := s.decrypt(row.DigitalRMBCodeCipher); decryptErr == nil { if value, decryptErr := s.decrypt(row.DigitalRMBCodeCipher); decryptErr == nil {
digitalRMBCode = value digitalRMBCode = value
@@ -337,7 +346,7 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
} }
items = append(items, PaymentAccountDetail{ items = append(items, PaymentAccountDetail{
ID: row.ID, BankName: row.BankName, HolderName: holderName, ID: row.ID, BankName: row.BankName, HolderName: holderName,
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL, WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
WechatQRAttachmentID: row.WechatQRAttachmentID, AlipayQRAttachmentID: row.AlipayQRAttachmentID, WechatQRAttachmentID: row.WechatQRAttachmentID, AlipayQRAttachmentID: row.AlipayQRAttachmentID,
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1, Masked: masked, VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1, Masked: masked,
@@ -386,10 +395,6 @@ func (s *Service) paymentAccountCreation(tenantID, memberID int64, input Payment
if err != nil { if err != nil {
return store.PaymentAccountCreation{}, err return store.PaymentAccountCreation{}, err
} }
identityNoCipher, err := s.encrypt(input.IdentityNo)
if err != nil {
return store.PaymentAccountCreation{}, err
}
accountCipher, err := s.encrypt(input.AccountNo) accountCipher, err := s.encrypt(input.AccountNo)
if err != nil { if err != nil {
return store.PaymentAccountCreation{}, err return store.PaymentAccountCreation{}, err
@@ -406,9 +411,8 @@ func (s *Service) paymentAccountCreation(tenantID, memberID int64, input Payment
return store.PaymentAccountCreation{ return store.PaymentAccountCreation{
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName, TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
HolderCipher: holderCipher, HolderMask: maskName(input.HolderName), HolderCipher: holderCipher, HolderMask: maskName(input.HolderName),
IdentityNoCipher: identityNoCipher, IdentityNoHash: s.lookupHash(input.IdentityNo), AccountNoCipher: accountCipher,
IdentityNoMask: maskIdentityNo(input.IdentityNo), AccountNoCipher: accountCipher, AccountNoHash: s.lookupHash(input.AccountNo), AccountNoMask: maskAccount(input.AccountNo),
AccountNoHash: s.lookupHash(input.AccountNo), AccountNoMask: maskAccount(input.AccountNo),
DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: digitalRMBCodeHash, DigitalRMBCodeCipher: digitalRMBCodeCipher, DigitalRMBCodeHash: digitalRMBCodeHash,
DigitalRMBCodeMask: digitalRMBCodeMask, DigitalRMBCodeMask: digitalRMBCodeMask,
WechatQRAttachmentID: input.WechatQRAttachmentID, WechatQRAttachmentID: input.WechatQRAttachmentID,
@@ -454,13 +458,6 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
if err != nil { if err != nil {
return nil, store.ErrPaymentAccountInvalid return nil, store.ErrPaymentAccountInvalid
} }
identityNo := ""
if account.IdentityNoCipher != "" {
identityNo, err = s.decrypt(account.IdentityNoCipher)
if err != nil {
return nil, store.ErrPaymentAccountInvalid
}
}
digitalRMBCode := "" digitalRMBCode := ""
if account.DigitalRMBCodeCipher != "" { if account.DigitalRMBCodeCipher != "" {
digitalRMBCode, err = s.decrypt(account.DigitalRMBCodeCipher) digitalRMBCode, err = s.decrypt(account.DigitalRMBCodeCipher)
@@ -471,7 +468,7 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
snapshot, err := json.Marshal(map[string]any{ snapshot, err := json.Marshal(map[string]any{
"account_type": accountTypeName(account.AccountType), "bank_code": account.BankCode, "account_type": accountTypeName(account.AccountType), "bank_code": account.BankCode,
"bank_name": account.BankName, "holder_name": holderName, "bank_name": account.BankName, "holder_name": holderName,
"identity_no": identityNo, "account_no": accountNo, "account_no": accountNo,
"digital_rmb_code": digitalRMBCode, "digital_rmb_code": digitalRMBCode,
"wechat_qr_attachment_id": account.WechatQRAttachmentID, "wechat_qr_attachment_id": account.WechatQRAttachmentID,
"alipay_qr_attachment_id": account.AlipayQRAttachmentID, "alipay_qr_attachment_id": account.AlipayQRAttachmentID,
@@ -849,10 +846,9 @@ func (s *Service) verifyTradePassword(ctx context.Context, tenantID, memberID in
func normalizePaymentAccount(input *PaymentAccountInput) error { func normalizePaymentAccount(input *PaymentAccountInput) error {
input.BankName = strings.TrimSpace(input.BankName) input.BankName = strings.TrimSpace(input.BankName)
input.HolderName = strings.TrimSpace(input.HolderName) input.HolderName = strings.TrimSpace(input.HolderName)
input.IdentityNo = strings.ToUpper(strings.NewReplacer(" ", "", "-", "").Replace(input.IdentityNo))
input.AccountNo = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.AccountNo)) input.AccountNo = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.AccountNo))
input.DigitalRMBCode = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.DigitalRMBCode)) input.DigitalRMBCode = strings.NewReplacer(" ", "", "-", "").Replace(strings.TrimSpace(input.DigitalRMBCode))
if !validHumanName(input.HolderName) || !identityNoPattern.MatchString(input.IdentityNo) || if !validHumanName(input.HolderName) ||
utf8.RuneCountInString(input.BankName) < 2 || utf8.RuneCountInString(input.BankName) > 100 || utf8.RuneCountInString(input.BankName) < 2 || utf8.RuneCountInString(input.BankName) > 100 ||
len(input.AccountNo) < 8 || len(input.AccountNo) > 32 || !allDigits(input.AccountNo) || len(input.AccountNo) < 8 || len(input.AccountNo) > 32 || !allDigits(input.AccountNo) ||
(input.DigitalRMBCode != "" && (len(input.DigitalRMBCode) < 6 || len(input.DigitalRMBCode) > 32 || !allDigits(input.DigitalRMBCode))) || (input.DigitalRMBCode != "" && (len(input.DigitalRMBCode) < 6 || len(input.DigitalRMBCode) > 32 || !allDigits(input.DigitalRMBCode))) ||
@@ -865,7 +861,7 @@ func normalizePaymentAccount(input *PaymentAccountInput) error {
func paymentAccountFromStore(row store.PaymentAccount) PaymentAccount { func paymentAccountFromStore(row store.PaymentAccount) PaymentAccount {
return PaymentAccount{ID: row.ID, AccountType: accountTypeName(row.AccountType), BankCode: row.BankCode, return PaymentAccount{ID: row.ID, AccountType: accountTypeName(row.AccountType), BankCode: row.BankCode,
BankName: row.BankName, HolderMask: row.HolderMask, IdentityNoMask: row.IdentityNoMask, BankName: row.BankName, HolderMask: row.HolderMask,
AccountNoMask: row.AccountNoMask, DigitalRMBCodeMask: row.DigitalRMBCodeMask, AccountNoMask: row.AccountNoMask, DigitalRMBCodeMask: row.DigitalRMBCodeMask,
WechatQRAttachmentID: row.WechatQRAttachmentID, WechatQRAttachmentID: row.WechatQRAttachmentID,
AlipayQRAttachmentID: row.AlipayQRAttachmentID, AlipayQRAttachmentID: row.AlipayQRAttachmentID,
@@ -1042,11 +1038,17 @@ func (s *Service) decrypt(value string) (string, error) {
if err != nil || len(payload) <= s.aead.NonceSize() { if err != nil || len(payload) <= s.aead.NonceSize() {
return "", errors.New("encrypted value is invalid") return "", errors.New("encrypted value is invalid")
} }
plain, err := s.aead.Open(nil, payload[:s.aead.NonceSize()], payload[s.aead.NonceSize():], nil) aeads := append([]cipher.AEAD{s.aead}, s.legacyAEADs...)
if err != nil { for _, aead := range aeads {
return "", err if len(payload) <= aead.NonceSize() {
continue
}
plain, decryptErr := aead.Open(nil, payload[:aead.NonceSize()], payload[aead.NonceSize():], nil)
if decryptErr == nil {
return string(plain), nil
}
} }
return string(plain), nil return "", errors.New("encrypted value cannot be decrypted")
} }
func (s *Service) lookupHash(value string) string { func (s *Service) lookupHash(value string) string {
@@ -1116,13 +1118,6 @@ func maskAccount(value string) string {
return string(runes[:prefix]) + strings.Repeat("*", len(runes)-prefix-4) + string(runes[len(runes)-4:]) return string(runes[:prefix]) + strings.Repeat("*", len(runes)-prefix-4) + string(runes[len(runes)-4:])
} }
func maskIdentityNo(value string) string {
if len(value) <= 8 {
return strings.Repeat("*", len(value))
}
return value[:4] + strings.Repeat("*", len(value)-8) + value[len(value)-4:]
}
func validHumanName(value string) bool { func validHumanName(value string) bool {
count := utf8.RuneCountInString(value) count := utf8.RuneCountInString(value)
if count < 2 || count > 64 { if count < 2 || count > 64 {
+37 -11
View File
@@ -135,22 +135,21 @@ func TestQuoteHonorsExplicitDisabledWithdrawalSwitch(t *testing.T) {
func TestNormalizePaymentAccount(t *testing.T) { func TestNormalizePaymentAccount(t *testing.T) {
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三", input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
IdentityNo: "11010119900101123X", AccountNo: "6222 0000-0000 1234", AccountNo: "6222 0000-0000 1234",
DigitalRMBCode: "1234 5678", DigitalRMBCode: "1234 5678",
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9} WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
if err := normalizePaymentAccount(&input); err != nil { if err := normalizePaymentAccount(&input); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if input.IdentityNo != "11010119900101123X" || input.AccountNo != "6222000000001234" || if input.AccountNo != "6222000000001234" || input.DigitalRMBCode != "12345678" ||
input.DigitalRMBCode != "12345678" || maskAccount(input.AccountNo) != "6222********1234" {
maskAccount(input.AccountNo) != "6222********1234" || maskIdentityNo(input.IdentityNo) != "1101**********123X" {
t.Fatalf("unexpected normalized input: %#v", input) t.Fatalf("unexpected normalized input: %#v", input)
} }
} }
func TestNormalizePaymentAccountAllowsEmptyDigitalRMBCode(t *testing.T) { func TestNormalizePaymentAccountAllowsEmptyDigitalRMBCode(t *testing.T) {
input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三", input := PaymentAccountInput{BankName: "中国工商银行", HolderName: "张三",
IdentityNo: "11010119900101123X", AccountNo: "6222000000001234", AccountNo: "6222000000001234",
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9} WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9}
if err := normalizePaymentAccount(&input); err != nil { if err := normalizePaymentAccount(&input); err != nil {
t.Fatalf("empty digital RMB code should be optional: %v", err) t.Fatalf("empty digital RMB code should be optional: %v", err)
@@ -167,11 +166,10 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters", FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
}) })
holder, _ := service.encrypt("张三") holder, _ := service.encrypt("张三")
identity, _ := service.encrypt("11010119900101123X")
account, _ := service.encrypt("6222000000001234") account, _ := service.encrypt("6222000000001234")
digital, _ := service.encrypt("123456") digital, _ := service.encrypt("123456")
repository.details = []store.PaymentAccount{{ repository.details = []store.PaymentAccount{{
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity, ID: 7, BankName: "中国工商银行", HolderCipher: holder,
AccountNoCipher: account, DigitalRMBCodeCipher: digital, AccountNoCipher: account, DigitalRMBCodeCipher: digital,
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png", WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png",
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, VerifyStatus: 2, WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, VerifyStatus: 2,
@@ -180,7 +178,7 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" || if len(items) != 1 || items[0].HolderName != "张三" ||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" || items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" || items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" ||
items[0].WechatQRAttachmentID != 8 || items[0].AlipayQRAttachmentID != 9 { items[0].WechatQRAttachmentID != 8 || items[0].AlipayQRAttachmentID != 9 {
@@ -188,13 +186,38 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
} }
} }
func TestPaymentAccountDetailsDecryptsWithLegacyMasterKey(t *testing.T) {
repository := &fakeRepository{}
legacyMasterKey := "legacy-finance-master-key-with-at-least-32-characters"
legacyService := New(repository, &redis.Client{}, config.AuthConfig{FinanceMasterKey: legacyMasterKey})
holder, _ := legacyService.encrypt("张三")
account, _ := legacyService.encrypt("6222000000001234")
digital, _ := legacyService.encrypt("123456")
repository.details = []store.PaymentAccount{{
ID: 7, BankName: "中国工商银行", HolderCipher: holder, AccountNoCipher: account,
DigitalRMBCodeCipher: digital, WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
}}
service := New(repository, &redis.Client{}, config.AuthConfig{
FinanceMasterKey: "current-finance-master-key-with-at-least-32-characters",
FinanceLegacyMasterKeys: []string{legacyMasterKey},
})
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || items[0].Masked || items[0].HolderName != "张三" ||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" {
t.Fatalf("unexpected legacy account details: %#v", items)
}
}
func TestUpdatePaymentAccountEncryptsEditedFields(t *testing.T) { func TestUpdatePaymentAccountEncryptsEditedFields(t *testing.T) {
repository := &paymentAccountRepository{} repository := &paymentAccountRepository{}
service := New(repository, &redis.Client{}, config.AuthConfig{ service := New(repository, &redis.Client{}, config.AuthConfig{
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters", FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
}) })
result, err := service.UpdatePaymentAccount(context.Background(), 1, 2, PaymentAccountInput{ result, err := service.UpdatePaymentAccount(context.Background(), 1, 2, PaymentAccountInput{
ID: 7, BankName: "中国工商银行", HolderName: "李四", IdentityNo: "11010119900101123X", ID: 7, BankName: "中国工商银行", HolderName: "李四",
AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
}) })
if err != nil { if err != nil {
@@ -226,7 +249,7 @@ func TestPaymentAccountDetailsFallsBackToMasksWhenHistoricalKeyIsUnavailable(t *
t.Fatal(err) t.Fatal(err)
} }
if len(items) != 1 || !items[0].Masked || items[0].HolderName != "张*" || if len(items) != 1 || !items[0].Masked || items[0].HolderName != "张*" ||
items[0].IdentityNo != "1101**********123X" || items[0].AccountNo != "6222********1234" || items[0].AccountNo != "6222********1234" ||
items[0].DigitalRMBCode != "12****78" { items[0].DigitalRMBCode != "12****78" {
t.Fatalf("unexpected masked fallback: %#v", items) t.Fatalf("unexpected masked fallback: %#v", items)
} }
@@ -238,7 +261,7 @@ func TestCreatePaymentAccountDoesNotStoreEmptyDigitalRMBCode(t *testing.T) {
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters", FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
}) })
_, err := service.CreatePaymentAccount(context.Background(), 0, 1, PaymentAccountInput{ _, err := service.CreatePaymentAccount(context.Background(), 0, 1, PaymentAccountInput{
BankName: "中国工商银行", HolderName: "张三", IdentityNo: "11010119900101123X", BankName: "中国工商银行", HolderName: "张三",
AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
}) })
if err != nil { if err != nil {
@@ -247,6 +270,9 @@ func TestCreatePaymentAccountDoesNotStoreEmptyDigitalRMBCode(t *testing.T) {
if repository.created.DigitalRMBCodeCipher != "" || repository.created.DigitalRMBCodeHash != "" || repository.created.DigitalRMBCodeMask != "" { if repository.created.DigitalRMBCodeCipher != "" || repository.created.DigitalRMBCodeHash != "" || repository.created.DigitalRMBCodeMask != "" {
t.Fatalf("empty digital RMB code must not be encrypted, hashed or masked: %#v", repository.created) t.Fatalf("empty digital RMB code must not be encrypted, hashed or masked: %#v", repository.created)
} }
if repository.created.IdentityNoCipher != "" || repository.created.IdentityNoHash != "" || repository.created.IdentityNoMask != "" {
t.Fatalf("identity number must no longer be collected: %#v", repository.created)
}
} }
func TestMaskAccountShortValueDoesNotLeak(t *testing.T) { func TestMaskAccountShortValueDoesNotLeak(t *testing.T) {
+2 -13
View File
@@ -52,15 +52,10 @@
<AsyncState v-else-if="formType === 'bank'" :loading="bankLoading" :error="bankLoadError" @retry="loadPaymentAccounts"> <AsyncState v-else-if="formType === 'bank'" :loading="bankLoading" :error="bankLoadError" @retry="loadPaymentAccounts">
<form class="m-plain-form m-bank-form" @submit.prevent="saveBank"> <form class="m-plain-form m-bank-form" @submit.prevent="saveBank">
<p v-if="existingAccount?.masked" class="m-bound-account-hint">历史资料无法完整读取,请重新填写姓名、身份证号和银行卡号后保存</p> <p v-if="existingAccount?.masked" class="m-bound-account-hint">历史资料无法完整读取,请重新填写姓名和银行卡号后保存</p>
<label <label
><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名" ><span>姓名</span><input v-model.trim="bank.holder_name" placeholder="请输入姓名"
/></label> /></label>
<label
><span>身份证号</span
><input v-model.trim="bank.identity_no" maxlength="18" placeholder="请输入你的身份证号"
/></label>
<div class="m-bank-form__divider"></div>
<label <label
><span>银行卡号</span ><span>银行卡号</span
><input v-model.trim="bank.account_no" inputmode="numeric" placeholder="请输入你的银行卡号" ><input v-model.trim="bank.account_no" inputmode="numeric" placeholder="请输入你的银行卡号"
@@ -266,7 +261,6 @@
id: number; id: number;
bank_name: string; bank_name: string;
holder_name: string; holder_name: string;
identity_no: string;
account_no: string; account_no: string;
digital_rmb_code: string; digital_rmb_code: string;
wechat_qr_url: string; wechat_qr_url: string;
@@ -282,7 +276,6 @@
const bank = reactive({ const bank = reactive({
bank_name: '', bank_name: '',
holder_name: '', holder_name: '',
identity_no: '',
account_no: '', account_no: '',
digital_rmb_code: '', digital_rmb_code: '',
wechat_qr_attachment_id: 0, wechat_qr_attachment_id: 0,
@@ -344,7 +337,7 @@
bankLoadError.value = ''; bankLoadError.value = '';
error.value = ''; error.value = '';
existingAccount.value = null; existingAccount.value = null;
Object.assign(bank, { bank_name: '', holder_name: '', identity_no: '', account_no: '', digital_rmb_code: '', Object.assign(bank, { bank_name: '', holder_name: '', account_no: '', digital_rmb_code: '',
wechat_qr_attachment_id: 0, alipay_qr_attachment_id: 0 }); wechat_qr_attachment_id: 0, alipay_qr_attachment_id: 0 });
qr.wechat.url = ''; qr.wechat.url = '';
qr.alipay.url = ''; qr.alipay.url = '';
@@ -359,7 +352,6 @@
bank.alipay_qr_attachment_id = account.alipay_qr_attachment_id; bank.alipay_qr_attachment_id = account.alipay_qr_attachment_id;
if (!account.masked) { if (!account.masked) {
bank.holder_name = account.holder_name; bank.holder_name = account.holder_name;
bank.identity_no = account.identity_no;
bank.account_no = account.account_no; bank.account_no = account.account_no;
bank.digital_rmb_code = account.digital_rmb_code; bank.digital_rmb_code = account.digital_rmb_code;
} }
@@ -371,14 +363,11 @@
} }
async function saveBank() { async function saveBank() {
error.value = ''; error.value = '';
const identityNo = bank.identity_no.replace(/[ -]/g, '');
const accountNo = bank.account_no.replace(/[ -]/g, ''); const accountNo = bank.account_no.replace(/[ -]/g, '');
const digitalRMBCode = bank.digital_rmb_code.replace(/[ -]/g, ''); const digitalRMBCode = bank.digital_rmb_code.replace(/[ -]/g, '');
const validationError = const validationError =
bank.holder_name.length < 2 bank.holder_name.length < 2
? '请输入至少 2 个字符的姓名' ? '请输入至少 2 个字符的姓名'
: !/^[1-9]\d{14}(?:\d{2}[\dXx])?$/.test(identityNo)
? '请输入正确的 15 位或 18 位身份证号'
: !/^\d{8,32}$/.test(accountNo) : !/^\d{8,32}$/.test(accountNo)
? '请输入 8 至 32 位银行卡号' ? '请输入 8 至 32 位银行卡号'
: bank.bank_name.length < 2 : bank.bank_name.length < 2