Merge pull request #157 from farazdagi/feature/verify-account

Feature: verify account password (w/o running node)
This commit is contained in:
Roman Volosovskyi 2017-05-16 10:38:32 +03:00 committed by GitHub
commit e54f5831a3
5 changed files with 197 additions and 27 deletions

View File

@ -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

View File

@ -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{}

View File

@ -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).

View File

@ -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 {

View File

@ -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)