fix: restore payment account details
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user