diff --git a/cmd/statusd/library.go b/cmd/statusd/library.go index 76b007e35..5d69de63e 100644 --- a/cmd/statusd/library.go +++ b/cmd/statusd/library.go @@ -77,44 +77,25 @@ func RecoverAccount(password, mnemonic *C.char) *C.char { return C.CString(string(outBytes)) } +//export VerifyAccountPassword +func VerifyAccountPassword(keyStoreDir, address, password *C.char) *C.char { + _, err := geth.VerifyAccountPassword(C.GoString(keyStoreDir), C.GoString(address), C.GoString(password)) + return makeJSONErrorResponse(err) +} + //export Login func Login(address, password *C.char) *C.char { // loads a key file (for a given address), tries to decrypt it using the password, to verify ownership // if verified, purges all the previous identities from Whisper, and injects verified key as shh identity err := geth.SelectAccount(C.GoString(address), C.GoString(password)) - - errString := "" - if err != nil { - fmt.Fprintln(os.Stderr, err) - errString = err.Error() - } - - out := geth.JSONError{ - Error: errString, - } - outBytes, _ := json.Marshal(&out) - - return C.CString(string(outBytes)) + return makeJSONErrorResponse(err) } //export Logout func Logout() *C.char { - // This is equivalent to clearing whisper identities err := geth.Logout() - - errString := "" - if err != nil { - fmt.Fprintln(os.Stderr, err) - errString = err.Error() - } - - out := geth.JSONError{ - Error: errString, - } - outBytes, _ := json.Marshal(&out) - - return C.CString(string(outBytes)) + return makeJSONErrorResponse(err) } //export CompleteTransaction diff --git a/cmd/statusd/utils.go b/cmd/statusd/utils.go index f70d4b32e..3fb516911 100644 --- a/cmd/statusd/utils.go +++ b/cmd/statusd/utils.go @@ -3,6 +3,7 @@ package main import "C" import ( "encoding/json" + "io/ioutil" "math/big" "os" "path/filepath" @@ -58,6 +59,10 @@ func testExportedAPI(t *testing.T, done chan struct{}) { "create main and child accounts", testCreateChildAccount, }, + { + "verify account password", + testVerifyAccountPassword, + }, { "recover account", testRecoverAccount, @@ -105,6 +110,45 @@ func testExportedAPI(t *testing.T, done chan struct{}) { done <- struct{}{} } +func testVerifyAccountPassword(t *testing.T) bool { + tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) // nolint: errcheck + + if err = geth.ImportTestAccount(tmpDir, "test-account1.pk"); err != nil { + t.Fatal(err) + } + if err = geth.ImportTestAccount(tmpDir, "test-account2.pk"); err != nil { + t.Fatal(err) + } + + // rename account file (to see that file's internals reviewed, when locating account key) + accountFilePathOriginal := filepath.Join(tmpDir, "test-account1.pk") + accountFilePath := filepath.Join(tmpDir, "foo"+testConfig.Account1.Address+"bar.pk") + if err := os.Rename(accountFilePathOriginal, accountFilePath); err != nil { + t.Fatal(err) + } + + response := geth.JSONError{} + rawResponse := VerifyAccountPassword( + C.CString(tmpDir), + C.CString(testConfig.Account1.Address), + C.CString(testConfig.Account1.Password)) + + if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil { + t.Errorf("cannot decode response (%s): %v", C.GoString(rawResponse), err) + return false + } + if response.Error != "" { + t.Errorf("unexpected error: %s", response.Error) + return false + } + + return true +} + func testGetDefaultConfig(t *testing.T) bool { // test Mainnet config nodeConfig := params.NodeConfig{} diff --git a/geth/accounts.go b/geth/accounts.go index 1da85ee5d..b81b7b0ef 100644 --- a/geth/accounts.go +++ b/geth/accounts.go @@ -1,10 +1,15 @@ package geth import ( + "bytes" "errors" "fmt" + "io/ioutil" + "os" + "path/filepath" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/status-im/status-go/extkeys" @@ -125,6 +130,56 @@ func RecoverAccount(password, mnemonic string) (address, pubKey string, err erro return address, pubKey, nil } +// VerifyAccountPassword tries to decrypt a given account key file, with a provided password. +// If no error is returned, then account is considered verified. +func VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) { + var err error + var keyJSON []byte + + addressObj := common.BytesToAddress(common.FromHex(address)) + checkAccountKey := func(path string, fileInfo os.FileInfo) error { + if len(keyJSON) > 0 || fileInfo.IsDir() { + return nil + } + + keyJSON, err = ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("invalid account key file: %v", err) + } + if !bytes.Contains(keyJSON, []byte(fmt.Sprintf(`"address":"%s"`, addressObj.Hex()[2:]))) { + keyJSON = []byte{} + } + + return nil + } + // locate key within key store directory (address should be within the file) + err = filepath.Walk(keyStoreDir, func(path string, fileInfo os.FileInfo, err error) error { + if err != nil { + return err + } + return checkAccountKey(path, fileInfo) + }) + if err != nil { + return nil, fmt.Errorf("cannot traverse key store folder: %v", err) + } + + if len(keyJSON) == 0 { + return nil, fmt.Errorf("cannot locate account for address: %x", addressObj) + } + + key, err := keystore.DecryptKey(keyJSON, password) + if err != nil { + return nil, err + } + + // avoid swap attack + if key.Address != addressObj { + return nil, fmt.Errorf("account mismatch: have %x, want %x", key.Address, addressObj) + } + + return key, nil +} + // SelectAccount selects current account, by verifying that address has corresponding account which can be decrypted // using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity, // all previous identities are removed). diff --git a/geth/accounts_test.go b/geth/accounts_test.go index 874069c08..9b587d670 100644 --- a/geth/accounts_test.go +++ b/geth/accounts_test.go @@ -2,12 +2,101 @@ package geth_test import ( "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" "reflect" "testing" + "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/geth" ) +func TestVerifyAccountPassword(t *testing.T) { + keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(keyStoreDir) // nolint: errcheck + + emptyKeyStoreDir, err := ioutil.TempDir(os.TempDir(), "empty") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(emptyKeyStoreDir) // nolint: errcheck + + // import account keys + if err = geth.ImportTestAccount(keyStoreDir, "test-account1.pk"); err != nil { + t.Fatal(err) + } + if err = geth.ImportTestAccount(keyStoreDir, "test-account2.pk"); err != nil { + t.Fatal(err) + } + + account1Address := common.BytesToAddress(common.FromHex(testConfig.Account1.Address)) + + testCases := []struct { + name string + keyPath string + address string + password string + expectedError error + }{ + { + "correct address, correct password (decrypt should succeed)", + keyStoreDir, + testConfig.Account1.Address, + testConfig.Account1.Password, + nil, + }, + { + "correct address, correct password, non-existent key store", + filepath.Join(keyStoreDir, "non-existent-folder"), + testConfig.Account1.Address, + testConfig.Account1.Password, + fmt.Errorf("cannot traverse key store folder: lstat %s/non-existent-folder: no such file or directory", keyStoreDir), + }, + { + "correct address, correct password, empty key store (pk is not there)", + emptyKeyStoreDir, + testConfig.Account1.Address, + testConfig.Account1.Password, + fmt.Errorf("cannot locate account for address: %x", account1Address), + }, + { + "wrong address, correct password", + keyStoreDir, + "0x79791d3e8f2daa1f7fec29649d152c0ada3cc535", + testConfig.Account1.Password, + fmt.Errorf("cannot locate account for address: %s", "79791d3e8f2daa1f7fec29649d152c0ada3cc535"), + }, + { + "correct address, wrong password", + keyStoreDir, + testConfig.Account1.Address, + "wrong password", // wrong password + errors.New("could not decrypt key with given passphrase"), + }, + } + for _, testCase := range testCases { + t.Log(testCase.name) + accountKey, err := geth.VerifyAccountPassword(testCase.keyPath, testCase.address, testCase.password) + if !reflect.DeepEqual(err, testCase.expectedError) { + t.Fatalf("unexpected error: expected \n'%v', got \n'%v'", testCase.expectedError, err) + } + if err == nil { + if accountKey == nil { + t.Error("no error reported, but account key is missing") + } + accountAddress := common.BytesToAddress(common.FromHex(testCase.address)) + if accountKey.Address != accountAddress { + t.Fatalf("account mismatch: have %x, want %x", accountKey.Address, accountAddress) + } + } + } +} + func TestAccountsList(t *testing.T) { err := geth.PrepareTestNode() if err != nil { diff --git a/static/config/linter_exclude_list.txt b/static/config/linter_exclude_list.txt index 7bfc49b87..113642d97 100644 --- a/static/config/linter_exclude_list.txt +++ b/static/config/linter_exclude_list.txt @@ -21,3 +21,4 @@ comment on exported function PopulateStaticPeers should be of the form "Populate comment on exported function AddPeer should be of the form "AddPeer ..." (golint) comment on exported function NotifyNode should be of the form "NotifyNode ..." (golint) comment on exported function TriggerTestSignal should be of the form "TriggerTestSignal ..." (golint) +comment on exported function VerifyAccountPassword should be of the form "VerifyAccountPassword ..." (golint)