diff --git a/VERSION b/VERSION index a1196bca7..74b901291 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.142.0 +0.142.1 diff --git a/server/pairing/client.go b/server/pairing/client.go index 3cb0cdbe3..e85a50665 100644 --- a/server/pairing/client.go +++ b/server/pairing/client.go @@ -239,6 +239,10 @@ func setupSendingClient(backend *api.GethStatusBackend, cs, configJSON string) ( if err != nil { return nil, err } + err = validateAndVerifyPassword(conf, conf.SenderConfig) + if err != nil { + return nil, err + } conf.SenderConfig.DB = backend.GetMultiaccountDB() @@ -437,6 +441,10 @@ func setupReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) if err != nil { return nil, err } + err = validateAndVerifyNodeConfig(conf, conf.ReceiverConfig) + if err != nil { + return nil, err + } conf.ReceiverConfig.DB = backend.GetMultiaccountDB() diff --git a/server/pairing/common.go b/server/pairing/common.go index de3065940..2333e7dcd 100644 --- a/server/pairing/common.go +++ b/server/pairing/common.go @@ -1,13 +1,51 @@ package pairing import ( + "fmt" + "io/fs" + "io/ioutil" "os" "path/filepath" + "reflect" + "regexp" + "strings" + + "gopkg.in/go-playground/validator.v9" "github.com/status-im/status-go/account/generator" "github.com/status-im/status-go/eth-node/keystore" ) +func newValidate() (*validator.Validate, error) { + var validate = validator.New() + var keyUIDPattern = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) + if err := validate.RegisterValidation("keyuid", func(fl validator.FieldLevel) bool { + return keyUIDPattern.MatchString(fl.Field().String()) + }); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("keystorepath", func(fl validator.FieldLevel) bool { + keyUIDField := fl.Parent() + if keyUIDField.Kind() == reflect.Ptr { + keyUIDField = keyUIDField.Elem() + } + + keyUID := keyUIDField.FieldByName("KeyUID").String() + return strings.HasSuffix(fl.Field().String(), keyUID) + }); err != nil { + return nil, err + } + + if err := validate.RegisterValidation("not_end_keyuid", func(fl validator.FieldLevel) bool { + keystorePath := fl.Field().String() + return len(keystorePath) <= 66 || !keyUIDPattern.MatchString(keystorePath[len(keystorePath)-66:]) + }); err != nil { + return nil, err + } + return validate, nil +} + func validateKeys(keys map[string][]byte, password string) error { for _, key := range keys { k, err := keystore.DecryptKey(key, password) @@ -24,6 +62,83 @@ func validateKeys(keys map[string][]byte, password string) error { return nil } +func loadKeys(keys map[string][]byte, keyStorePath string) error { + fileWalker := func(path string, dirEntry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if dirEntry.IsDir() || filepath.Dir(path) != keyStorePath { + return nil + } + + rawKeyFile, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("invalid account key file: %v", err) + } + + keys[dirEntry.Name()] = rawKeyFile + + return nil + } + + err := filepath.WalkDir(keyStorePath, fileWalker) + if err != nil { + return fmt.Errorf("cannot traverse key store folder: %v", err) + } + + return nil +} + +func validate(s interface{}) error { + v, err := newValidate() + if err != nil { + return err + } + + return v.Struct(s) +} + +func validateAndVerifyPassword(s interface{}, senderConfig *SenderConfig) error { + err := validate(s) + if err != nil { + return err + } + + keys := make(map[string][]byte) + err = loadKeys(keys, senderConfig.KeystorePath) + if err != nil { + return err + } + + return validateKeys(keys, senderConfig.Password) +} + +func validateAndVerifyNodeConfig(s interface{}, receiverConfig *ReceiverConfig) error { + if receiverConfig.NodeConfig != nil { + // we should update with defaults before validation + err := receiverConfig.NodeConfig.UpdateWithDefaults() + if err != nil { + return err + } + } + + err := validate(s) + if err != nil { + return err + } + + if receiverConfig.NodeConfig == nil { + return fmt.Errorf("node config is required for receiver config") + } + + if receiverConfig.NodeConfig.RootDataDir == "" { + return fmt.Errorf("root data dir is required for node config") + } + + return receiverConfig.NodeConfig.Validate() +} + func emptyDir(dir string) error { // Open the directory d, err := os.Open(dir) diff --git a/server/pairing/config.go b/server/pairing/config.go index c48f4c792..a1824b000 100644 --- a/server/pairing/config.go +++ b/server/pairing/config.go @@ -10,26 +10,29 @@ import ( type SenderConfig struct { // SenderConfig.KeystorePath must end with keyUID - KeystorePath string `json:"keystorePath"` + KeystorePath string `json:"keystorePath" validate:"required,keystorepath"` // DeviceType SendPairInstallation need this information - DeviceType string `json:"deviceType"` + DeviceType string `json:"deviceType" validate:"required"` - KeyUID string `json:"keyUID"` - Password string `json:"password"` + KeyUID string `json:"keyUID" validate:"required,keyuid"` + Password string `json:"password" validate:"required"` DB *multiaccounts.Database `json:"-"` } type ReceiverConfig struct { + // nodeConfig is required, but we'll validate it separately NodeConfig *params.NodeConfig `json:"nodeConfig"` // ReceiverConfig.KeystorePath must not end with keyUID (because keyUID is not known yet) - KeystorePath string `json:"keystorePath"` + KeystorePath string `json:"keystorePath" validate:"required,not_end_keyuid"` + // DeviceType SendPairInstallation need this information - DeviceType string `json:"deviceType"` - KDFIterations int `json:"kdfIterations"` + DeviceType string `json:"deviceType" validate:"required"` + KDFIterations int `json:"kdfIterations" validate:"gte=0"` + // SettingCurrentNetwork corresponding to field current_network from table settings, so that we can override current network from sender - SettingCurrentNetwork string `json:"settingCurrentNetwork"` + SettingCurrentNetwork string `json:"settingCurrentNetwork" validate:"required"` DB *multiaccounts.Database `json:"-"` LoggedInKeyUID string `json:"-"` @@ -37,7 +40,7 @@ type ReceiverConfig struct { type ServerConfig struct { // Timeout the number of milliseconds after which the pairing server will automatically terminate - Timeout uint `json:"timeout"` + Timeout uint `json:"timeout" validate:"omitempty,gte=0"` // Connection fields, not json (un)marshalled // Required for the server, but MUST NOT come from client @@ -51,23 +54,23 @@ type ServerConfig struct { type ClientConfig struct{} type SenderServerConfig struct { - SenderConfig *SenderConfig `json:"senderConfig"` - ServerConfig *ServerConfig `json:"serverConfig"` + SenderConfig *SenderConfig `json:"senderConfig" validate:"required"` + ServerConfig *ServerConfig `json:"serverConfig" validate:"omitempty,dive"` } type SenderClientConfig struct { - SenderConfig *SenderConfig `json:"senderConfig"` + SenderConfig *SenderConfig `json:"senderConfig" validate:"required"` ClientConfig *ClientConfig `json:"clientConfig"` } type ReceiverClientConfig struct { - ReceiverConfig *ReceiverConfig `json:"receiverConfig"` + ReceiverConfig *ReceiverConfig `json:"receiverConfig" validate:"required"` ClientConfig *ClientConfig `json:"clientConfig"` } type ReceiverServerConfig struct { - ReceiverConfig *ReceiverConfig `json:"receiverConfig"` - ServerConfig *ServerConfig `json:"serverConfig"` + ReceiverConfig *ReceiverConfig `json:"receiverConfig" validate:"required"` + ServerConfig *ServerConfig `json:"serverConfig" validate:"omitempty,dive"` } func NewSenderServerConfig() *SenderServerConfig { diff --git a/server/pairing/config_test.go b/server/pairing/config_test.go new file mode 100644 index 000000000..dfc54c950 --- /dev/null +++ b/server/pairing/config_test.go @@ -0,0 +1,103 @@ +package pairing + +import ( + "testing" + + "github.com/stretchr/testify/suite" + "gopkg.in/go-playground/validator.v9" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigTestSuite(t *testing.T) { + suite.Run(t, new(ConfigTestSuite)) +} + +type ConfigTestSuite struct { + suite.Suite + validate *validator.Validate +} + +func (s *ConfigTestSuite) SetupTest() { + var err error + s.validate, err = newValidate() + require.NoError(s.T(), err, "newValidate should not return error") +} + +func (s *ConfigTestSuite) TestValidationKeystorePath() { + s.T().Run("Valid keystore path with keyUID", func(t *testing.T) { + sc := &SenderConfig{ + KeystorePath: "some/path/0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + DeviceType: "phone", + KeyUID: "0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + Password: "password", + } + + assert.NoError(t, s.validate.Struct(sc), "SenderConfig validation should pass") + }) + + s.T().Run("Invalid keystore path without keyUID", func(t *testing.T) { + sc := &SenderConfig{ + KeystorePath: "some/path/", + DeviceType: "phone", + KeyUID: "0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + Password: "password", + } + + assert.Error(t, s.validate.Struct(sc), "SenderConfig validation should fail") + }) +} + +func (s *ConfigTestSuite) TestValidationKeyUID() { + s.T().Run("Valid keyUID", func(t *testing.T) { + sc := &SenderConfig{ + KeystorePath: "some/path/0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + DeviceType: "phone", + KeyUID: "0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + Password: "password", + } + + assert.NoError(t, s.validate.Struct(sc), "SenderConfig validation should pass") + }) + + s.T().Run("Invalid keyUID", func(t *testing.T) { + sc := &SenderConfig{ + KeystorePath: "some/path/0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + DeviceType: "phone", + KeyUID: "0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54", + Password: "password", + } + + assert.Error(t, s.validate.Struct(sc), "SenderConfig validation should fail") + }) +} + +func (s *ConfigTestSuite) TestValidationNotEndKeyUID() { + nodeConfig, err := defaultNodeConfig(uuid.New().String(), "") + nodeConfig.RootDataDir = "/tmp" + require.NoError(s.T(), err, "defaultNodeConfig should not return error") + s.T().Run("Valid keystore path without keyUID", func(t *testing.T) { + r := &ReceiverConfig{ + NodeConfig: nodeConfig, + KeystorePath: "some/path/", + DeviceType: "phone", + KDFIterations: 1, + SettingCurrentNetwork: "mainnet", + } + + assert.NoError(t, validateAndVerifyNodeConfig(r, r), "ReceiverConfig validation should pass") + }) + + s.T().Run("Invalid keystore path with keyUID", func(t *testing.T) { + r := &ReceiverConfig{ + NodeConfig: nodeConfig, + KeystorePath: "some/path/0x130cc0ebdaecd220c1d6dea0ef01d575ef5364506785745049eb98ddf49cb54e", + DeviceType: "phone", + KDFIterations: 1, + SettingCurrentNetwork: "mainnet", + } + assert.Error(t, validateAndVerifyNodeConfig(r, r), "ReceiverConfig validation should fail") + }) +} diff --git a/server/pairing/payload_mounter.go b/server/pairing/payload_mounter.go index 8ec8ea391..62f4eb51b 100644 --- a/server/pairing/payload_mounter.go +++ b/server/pairing/payload_mounter.go @@ -1,11 +1,6 @@ package pairing import ( - "fmt" - "io/fs" - "io/ioutil" - "path/filepath" - "github.com/golang/protobuf/proto" "go.uber.org/zap" @@ -126,7 +121,8 @@ func NewAccountPayloadLoader(p *AccountPayload, config *SenderConfig) (*AccountP } func (apl *AccountPayloadLoader) Load() error { - err := apl.loadKeys(apl.keystorePath) + apl.keys = make(map[string][]byte) + err := loadKeys(apl.keys, apl.keystorePath) if err != nil { return err } @@ -144,36 +140,6 @@ func (apl *AccountPayloadLoader) Load() error { return nil } -func (apl *AccountPayloadLoader) loadKeys(keyStorePath string) error { - apl.keys = make(map[string][]byte) - - fileWalker := func(path string, dirEntry fs.DirEntry, err error) error { - if err != nil { - return err - } - - if dirEntry.IsDir() || filepath.Dir(path) != keyStorePath { - return nil - } - - rawKeyFile, err := ioutil.ReadFile(path) - if err != nil { - return fmt.Errorf("invalid account key file: %v", err) - } - - apl.keys[dirEntry.Name()] = rawKeyFile - - return nil - } - - err := filepath.WalkDir(keyStorePath, fileWalker) - if err != nil { - return fmt.Errorf("cannot traverse key store folder: %v", err) - } - - return nil -} - /* |-------------------------------------------------------------------------- | RawMessagePayload diff --git a/server/pairing/server.go b/server/pairing/server.go index 6eb1c1973..81973f9f7 100644 --- a/server/pairing/server.go +++ b/server/pairing/server.go @@ -172,6 +172,10 @@ func StartUpSenderServer(backend *api.GethStatusBackend, configJSON string) (str if err != nil { return "", err } + err = validateAndVerifyPassword(conf, conf.SenderConfig) + if err != nil { + return "", err + } ps, err := MakeFullSenderServer(backend, conf) if err != nil { @@ -268,6 +272,10 @@ func StartUpReceiverServer(backend *api.GethStatusBackend, configJSON string) (s if err != nil { return "", err } + err = validateAndVerifyNodeConfig(conf, conf.ReceiverConfig) + if err != nil { + return "", err + } ps, err := MakeFullReceiverServer(backend, conf) if err != nil { diff --git a/server/pairing/sync_device_test.go b/server/pairing/sync_device_test.go index 07bdc8fc7..99b90a11b 100644 --- a/server/pairing/sync_device_test.go +++ b/server/pairing/sync_device_test.go @@ -363,13 +363,9 @@ func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derive visibleTokenJSONRaw := json.RawMessage(visibleTokensJSON) settings.WalletVisibleTokens = &visibleTokenJSONRaw - networks := make([]map[string]string, 0) - networksJSON, err := json.Marshal(networks) - if err != nil { - return nil, err - } - networkRawMessage := json.RawMessage(networksJSON) - settings.Networks = &networkRawMessage + networks := `[{"id":"goerli_rpc","chain-explorer-link":"https://goerli.etherscan.io/address/","name":"Goerli with upstream RPC","config":{"NetworkId":5,"DataDir":"/ethereum/goerli_rpc","UpstreamConfig":{"Enabled":true,"URL":"https://goerli-archival.gateway.pokt.network/v1/lb/3ef2018191814b7e1009b8d9"}}},{"id":"mainnet_rpc","chain-explorer-link":"https://etherscan.io/address/","name":"Mainnet with upstream RPC","config":{"NetworkId":1,"DataDir":"/ethereum/mainnet_rpc","UpstreamConfig":{"Enabled":true,"URL":"https://eth-archival.gateway.pokt.network/v1/lb/3ef2018191814b7e1009b8d9"}}}]` + var networksRawMessage json.RawMessage = []byte(networks) + settings.Networks = &networksRawMessage settings.CurrentNetwork = currentNetwork return settings, nil @@ -407,7 +403,7 @@ func defaultNodeConfig(installationID, keyUID string) (*params.NodeConfig, error } nodeConfig.ShhextConfig = params.ShhextConfig{ - BackupDisabledDataDir: "", + BackupDisabledDataDir: nodeConfig.DataDir, InstallationID: installationID, MaxMessageDeliveryAttempts: 6, MailServerConfirmations: true,