diff options
author | Péter Szilágyi <peterke@gmail.com> | 2017-02-08 21:53:02 +0800 |
---|---|---|
committer | Péter Szilágyi <peterke@gmail.com> | 2017-02-13 20:00:08 +0800 |
commit | c5215fdd48231622dd56aba63a5187c6e42828d4 (patch) | |
tree | 98bdb47dccb9ef3deaa571585c32db9d19166a80 /accounts/keystore | |
parent | fad5eb0a87abfc12812647344a26de8a43830182 (diff) | |
download | dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.gz dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.bz2 dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.lz dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.xz dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.tar.zst dexon-c5215fdd48231622dd56aba63a5187c6e42828d4.zip |
accounts, cmd, internal, mobile, node: canonical account URLs
Diffstat (limited to 'accounts/keystore')
-rw-r--r-- | accounts/keystore/account_cache.go | 14 | ||||
-rw-r--r-- | accounts/keystore/account_cache_test.go | 49 | ||||
-rw-r--r-- | accounts/keystore/key.go | 4 | ||||
-rw-r--r-- | accounts/keystore/keystore.go | 20 | ||||
-rw-r--r-- | accounts/keystore/keystore_plain_test.go | 8 | ||||
-rw-r--r-- | accounts/keystore/keystore_test.go | 10 | ||||
-rw-r--r-- | accounts/keystore/keystore_wallet.go | 25 | ||||
-rw-r--r-- | accounts/keystore/presale.go | 4 |
8 files changed, 68 insertions, 66 deletions
diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index cc8626afc..e2f826250 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -42,7 +42,7 @@ const minReloadInterval = 2 * time.Second type accountsByURL []accounts.Account func (s accountsByURL) Len() int { return len(s) } -func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } +func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 } func (s accountsByURL) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // AmbiguousAddrError is returned when attempting to unlock @@ -55,7 +55,7 @@ type AmbiguousAddrError struct { func (err *AmbiguousAddrError) Error() string { files := "" for i, a := range err.Matches { - files += a.URL + files += a.URL.Path if i < len(err.Matches)-1 { files += ", " } @@ -104,7 +104,7 @@ func (ac *accountCache) add(newAccount accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL.Cmp(newAccount.URL) >= 0 }) if i < len(ac.all) && ac.all[i] == newAccount { return } @@ -155,10 +155,10 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { if (a.Address != common.Address{}) { matches = ac.byAddr[a.Address] } - if a.URL != "" { + if a.URL.Path != "" { // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.URL, filepath.Separator) { - a.URL = filepath.Join(ac.keydir, a.URL) + if !strings.ContainsRune(a.URL.Path, filepath.Separator) { + a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) } for i := range matches { if matches[i].URL == a.URL { @@ -272,7 +272,7 @@ func (ac *accountCache) scan() ([]accounts.Account, error) { case (addr == common.Address{}): glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) default: - addrs = append(addrs, accounts.Account{Address: addr, URL: path}) + addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}}) } fd.Close() } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index ea6f7d011..3e68351ea 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -37,15 +37,15 @@ var ( cachetestAccounts = []accounts.Account{ { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(cachetestDir, "aaa"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: filepath.Join(cachetestDir, "zzz"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, }, } ) @@ -63,10 +63,11 @@ func TestWatchNewFile(t *testing.T) { // Move in the files. wantAccounts := make([]accounts.Account, len(cachetestAccounts)) for i := range cachetestAccounts { - a := cachetestAccounts[i] - a.URL = filepath.Join(dir, filepath.Base(a.URL)) - wantAccounts[i] = a - if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { + wantAccounts[i] = accounts.Account{ + Address: cachetestAccounts[i].Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, + } + if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { t.Fatal(err) } } @@ -107,13 +108,13 @@ func TestWatchNoDir(t *testing.T) { os.MkdirAll(dir, 0700) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { t.Fatal(err) } // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = file + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { @@ -145,31 +146,31 @@ func TestCacheAddDeleteOrder(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: "-309830980", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: "ggg", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, }, { Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - URL: "zzzzzz-the-very-last-one.keyXXX", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: "SOMETHING.key", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, }, { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: "aaa", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: "zzz", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, }, } for _, a := range accs { @@ -210,7 +211,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } - cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) select { case <-notify: @@ -245,19 +246,19 @@ func TestCacheFind(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: filepath.Join(dir, "a.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: filepath.Join(dir, "b.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c2.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, }, } for _, a := range accs { @@ -266,7 +267,7 @@ func TestCacheFind(t *testing.T) { nomatchAccount := accounts.Account{ Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(dir, "something"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, } tests := []struct { Query accounts.Account @@ -278,7 +279,7 @@ func TestCacheFind(t *testing.T) { // by file {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, // by basename - {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, // by file and address {Query: accs[0], WantResult: accs[0]}, // ambiguous address, tie resolved by file @@ -294,7 +295,7 @@ func TestCacheFind(t *testing.T) { // no match error {Query: nomatchAccount, WantError: ErrNoMatch}, {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, - {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, WantError: ErrNoMatch}, {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, } for i, test := range tests { diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 1cf5f9366..e2bdf09fe 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -181,8 +181,8 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou if err != nil { return nil, accounts.Account{}, err } - a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} - if err := ks.StoreKey(a.URL, key, auth); err != nil { + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}} + if err := ks.StoreKey(a.URL.Path, key, auth); err != nil { zeroKey(key.PrivateKey) return nil, a, err } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index ce4e87ce9..a01ff17e3 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -49,6 +49,9 @@ var ( // KeyStoreType is the reflect type of a keystore backend. var KeyStoreType = reflect.TypeOf(&KeyStore{}) +// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. +var KeyStoreScheme = "keystore" + // Maximum time between wallet refreshes (if filesystem notifications don't work). const walletRefreshCycle = 3 * time.Second @@ -130,22 +133,21 @@ func (ks *KeyStore) Wallets() []accounts.Wallet { // necessary wallet refreshes. func (ks *KeyStore) refreshWallets() { // Retrieve the current list of accounts + ks.mu.Lock() accs := ks.cache.accounts() // Transform the current list of wallets into the new one - ks.mu.Lock() - wallets := make([]accounts.Wallet, 0, len(accs)) events := []accounts.WalletEvent{} for _, account := range accs { // Drop wallets while they were in front of the next account - for len(ks.wallets) > 0 && ks.wallets[0].URL() < account.URL { + for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false}) ks.wallets = ks.wallets[1:] } // If there are no more wallets or the account is before the next, wrap new wallet - if len(ks.wallets) == 0 || ks.wallets[0].URL() > account.URL { + if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { wallet := &keystoreWallet{account: account, keystore: ks} events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) @@ -242,7 +244,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { // The order is crucial here. The key is dropped from the // cache after the file is gone so that a reload happening in // between won't insert it into the cache again. - err = os.Remove(a.URL) + err = os.Remove(a.URL.Path) if err == nil { ks.cache.delete(a) ks.refreshWallets() @@ -377,7 +379,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } - key, err := ks.storage.GetKey(a.Address, a.URL, auth) + key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) return a, key, err } @@ -453,8 +455,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} - if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil { + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { return accounts.Account{}, err } ks.cache.add(a) @@ -468,7 +470,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) if err != nil { return err } - return ks.storage.StoreKey(a.URL, key, newPassphrase) + return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores diff --git a/accounts/keystore/keystore_plain_test.go b/accounts/keystore/keystore_plain_test.go index dcb2ab901..8c0eb52ea 100644 --- a/accounts/keystore/keystore_plain_test.go +++ b/accounts/keystore/keystore_plain_test.go @@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt { + if _, err = ks.GetKey(k1.Address, account.URL.Path, "bar"); err != ErrDecrypt { t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) } } @@ -115,7 +115,7 @@ func TestImportPreSaleKey(t *testing.T) { if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { t.Errorf("imported account has wrong address %x", account.Address) } - if !strings.HasPrefix(account.URL, dir) { + if !strings.HasPrefix(account.URL.Path, dir) { t.Errorf("imported account file not in keystore directory: %q", account.URL) } } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 6b7170a2f..1f1935be6 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -41,10 +41,10 @@ func TestKeyStore(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(a.URL, dir) { + if !strings.HasPrefix(a.URL.Path, dir) { t.Errorf("account file %s doesn't have dir prefix", a.URL) } - stat, err := os.Stat(a.URL) + stat, err := os.Stat(a.URL.Path) if err != nil { t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) } @@ -60,7 +60,7 @@ func TestKeyStore(t *testing.T) { if err := ks.Delete(a, "bar"); err != nil { t.Errorf("Delete error: %v", err) } - if common.FileExist(a.URL) { + if common.FileExist(a.URL.Path) { t.Errorf("account file %s should be gone after Delete", a.URL) } if ks.HasAddress(a.Address) { @@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) { // Randomly add and remove account and make sure events and wallets are in sync live := make(map[common.Address]accounts.Account) - for i := 0; i < 1024; i++ { + for i := 0; i < 256; i++ { // Execute a creation or deletion and ensure event arrival if create := len(live) == 0 || rand.Int()%4 > 0; create { // Add a new account and ensure wallet notifications arrives @@ -349,8 +349,6 @@ func TestWalletNotifications(t *testing.T) { } } } - // Sleep a bit to avoid same-timestamp keyfiles - time.Sleep(10 * time.Millisecond) } } diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go index d92926478..7d5507a4f 100644 --- a/accounts/keystore/keystore_wallet.go +++ b/accounts/keystore/keystore_wallet.go @@ -30,20 +30,21 @@ type keystoreWallet struct { keystore *KeyStore // Keystore where the account originates from } -// Type implements accounts.Wallet, returning the textual type of the wallet. -func (w *keystoreWallet) Type() string { - return "secret-storage" -} - // URL implements accounts.Wallet, returning the URL of the account within. -func (w *keystoreWallet) URL() string { +func (w *keystoreWallet) URL() accounts.URL { return w.account.URL } // Status implements accounts.Wallet, always returning "open", since there is no // concept of open/close for plain keystore accounts. func (w *keystoreWallet) Status() string { - return "Open" + w.keystore.mu.RLock() + defer w.keystore.mu.RUnlock() + + if _, ok := w.keystore.unlocked[w.account.Address]; ok { + return "Unlocked" + } + return "Locked" } // Open implements accounts.Wallet, but is a noop for plain wallets since there @@ -63,7 +64,7 @@ func (w *keystoreWallet) Accounts() []accounts.Account { // Contains implements accounts.Wallet, returning whether a particular account is // or is not wrapped by this wallet instance. func (w *keystoreWallet) Contains(account accounts.Account) bool { - return account.Address == w.account.Address && (account.URL == "" || account.URL == w.account.URL) + return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL) } // Derive implements accounts.Wallet, but is a noop for plain wallets since there @@ -81,7 +82,7 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -97,7 +98,7 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -111,7 +112,7 @@ func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passph if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -125,7 +126,7 @@ func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphra if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index 948320e0a..5b883c45f 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -38,8 +38,8 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou return accounts.Account{}, nil, err } key.Id = uuid.NewRandom() - a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} - err = keyStore.StoreKey(a.URL, key, password) + a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}} + err = keyStore.StoreKey(a.URL.Path, key, password) return a, key, err } |