Skip to content
This repository was archived by the owner on Nov 6, 2020. It is now read-only.

Pyethereum keystore support #9710

Merged
merged 24 commits into from
Jan 3, 2019

Conversation

tworec
Copy link
Contributor

@tworec tworec commented Oct 5, 2018

Continuation of #9444
(sorry for duplicate, but I can not reopen #9444)

@parity-cla-bot
Copy link

It looks like this contributor signed our Contributor License Agreement. 👍

Many thanks,

Parity Technologies CLA Bot

@5chdn 5chdn added this to the 2.2 milestone Oct 8, 2018
@5chdn 5chdn added A0-pleasereview 🤓 Pull request needs code review. M4-core ⛓ Core client code / Rust. labels Oct 8, 2018
debris
debris previously requested changes Oct 10, 2018
Copy link
Collaborator

@debris debris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contribution! There are several blockers for this pull request. Most importantly badly designed KeyFileManager trait

let crypto = Crypto::from(json.crypto);
let address = match json.address {
Some(address) => address.into(),
None => match password {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the json file contains address and we provided a password? imo, this should result in an Error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I would silently ignore this. As for now all errors reading kestores are silently ignored, which is confusing me too, but to be consistent I propose to ignore it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sustain to ignore this case. There can be many key files in src dir, and just some of them need passwd.. rest should be ignored, even if rest is all.

@debris debris added A4-gotissues 💥 Pull request is reviewed and has significant issues which must be addressed. and removed A0-pleasereview 🤓 Pull request needs code review. labels Oct 10, 2018
@5chdn 5chdn added the A3-stale 🍃 Pull request did not receive any updates in a long time. No review needed at this stage. Close it. label Oct 26, 2018
@5chdn
Copy link
Contributor

5chdn commented Oct 26, 2018

Are you still working on this @tworec

@5chdn 5chdn modified the milestones: 2.2, 2.3 Oct 29, 2018
@5chdn 5chdn closed this Nov 25, 2018
@tworec
Copy link
Contributor Author

tworec commented Nov 26, 2018

I want to finish this. I was looking to see @debris answers...

@5chdn 5chdn reopened this Nov 27, 2018
@5chdn 5chdn added A3-inprogress ⏳ Pull request is in progress. No review needed at this stage. and removed A3-stale 🍃 Pull request did not receive any updates in a long time. No review needed at this stage. Close it. A4-gotissues 💥 Pull request is reviewed and has significant issues which must be addressed. labels Nov 27, 2018
@5chdn
Copy link
Contributor

5chdn commented Dec 4, 2018

👍 needs a 2nd review

Copy link
Collaborator

@niklasad1 niklasad1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey,

Can you please revert the changes eprintln! to ẁarn! but inorder to get the logger working for the cli you must enable it which it is not.

The easiest is to use env_logger

You need the following:

# ethstore/cli/Cargo.toml
env_logger = "0.5"
# ethstore/cli/src/main.rs
extern crate env_logger;
env_logger::try_init().expect("Logger initialized only once.");

@tworec
Copy link
Contributor Author

tworec commented Dec 19, 2018

@niklasad1 done, but I've initialized log level to warn also. It will enable logs for invalid password.

This is sample CLI output with invalid password

parity-ethereum/ethstore/cli pyethereum-keystore-support (0282536a4)$ cargo run import pwd.txx --src ~/ethstore/
   Compiling ethstore-cli v0.1.1 (/Users/tworec/git/parity-ethereum/ethstore/cli)                                                                                                    
    Finished dev [unoptimized + debuginfo] target(s) in 3.30s                                                                                                                        
     Running `/Users/tworec/git/parity-ethereum/target/debug/ethstore import pwd.txx --src /Users/tworec/ethstore/`
 WARN 2018-12-19T11:05:15Z: ethstore::accounts_dir::disk: Invalid key file: "/Users/tworec/ethstore/keystore-mainnet-backup.json" (Invalid password)
 WARN 2018-12-19T11:05:15Z: ethstore::accounts_dir::disk: Invalid key file: "/Users/tworec/ethstore/mainnet_keystore.json" (Invalid password)

@5chdn
Copy link
Contributor

5chdn commented Dec 28, 2018

@tworec could you rebase this?

@niklasad1 could you review this again?

@tworec
Copy link
Contributor Author

tworec commented Dec 28, 2018

@5chdn I think rebase is overkill here. I've merged instead.

@5chdn
Copy link
Contributor

5chdn commented Dec 28, 2018

yes, whatever you prefer :)

@tworec
Copy link
Contributor Author

tworec commented Dec 28, 2018

So why do we need a review? Merge introduced no changes in this PR

@5chdn
Copy link
Contributor

5chdn commented Jan 2, 2019

yes. one more review. please wait till this holiday craze is over.

@tworec
Copy link
Contributor Author

tworec commented Jan 2, 2019 via email

Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks decent, I have couple of grumbles regarding code pollution.

Since the password is required to import I think we should treat python directory as a vault if that's possible.

@@ -146,30 +148,34 @@ impl fmt::Display for Error {

fn main() {
panic_hook::set_abort();
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "warn")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Wouldn't it default to info anyway (which imho seems better than warn)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, see https://docs.rs/env_logger/0.5.0/env_logger/#enabling-logging

by default all logging is disabled except for error!

And I want to see this warning:
warn!("Invalid key file: {:?} ({})", path, err);

see discussion above


match execute(env::args()) {
Ok(result) => println!("{}", result),
Err(Error::Docopt(ref e)) => e.exit(),
Err(err) => {
println!("{}", err);
eprintln!("{}", err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe use error! since the logger is already initialized.

Copy link
Contributor Author

@tworec tworec Jan 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe, but see this #9710 (comment) by @debris

guys pls, can we just proceed and not nitpicking about error messages?


impl RootDiskDirectory {
pub fn create<P>(path: P) -> Result<Self, Error> where P: AsRef<Path> {
fs::create_dir_all(&path)?;
Ok(Self::at(path))
}

/// allows to read keyfiles with given password (needed for keyfiles w/o address)
pub fn with_password(&self, password: Option<Password>) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to implement that here. Reasons:

  1. The assumption is that all accounts inside the directory share the same password which is most likely incorrect.
  2. It's used only for python keyfiles import, but will polute (already convoluted) codebase

Can we not use DiskDirectory for python keyfiles import and just allow importing particular account files?
Alternatively can't we treat python keyfiles as a vault (they share the same password actually) and use structs from this code part?

Copy link
Contributor Author

@tworec tworec Jan 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ad1. I agree, it is not very generic, but as a user I just create special dir for my keyfiles (even if its single one) before import.

ad 2. it is used for all keyfiles w/o address. Not only pyethereum ones. As any new feature it introduces some noice.

Can we not use DiskDirectory for python keyfiles import and just allow importing particular account files?

I've tried to be consistent with current approach which is importing whole directories. Introduction of new import-single in CLI seems overkill to me.

Alternatively can't we treat python keyfiles as a vault (they share the same password actually) and use structs from this code part?

I've analyzed vault's, but it does not fit here. You need to repack your keyfiles into valuts, and in order to repack you need to import it first, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ad1. if your keyfiles have different pwds you will see warnings, but import will succeed with at least this file with pwd you've entered. You can reexecute import with another pwd to import others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see example session here #9710 (comment)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have import-wallet, but it seems it only works for presale wallets currently. I'd be more inclined to handle it there than to hack around DiskKeyFileManager (since the former command allows importing directly to a vault, etc).

That said I'm fine with this if you address the SafeAccount semantics in a way I proposed in the other comment.

pub fn from_file_with_password(json: json::KeyFile, filename: Option<String>, password: &Option<Password>) -> Result<Self, Error> {
let crypto = Crypto::from(json.crypto);
let address = match json.address {
Some(address) => address.into(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho if the password is provided we should validate that the adress is correct. Maybe switch the order here: first check if we have password and after deriving the address optionally compare it with the one in json.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice feature. It will need to throw an error when addresses do not match, but in next comment you're proposing to revert Result.

@@ -77,16 +77,32 @@ impl SafeAccount {
/// Create a new `SafeAccount` from the given `json`; if it was read from a
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
/// can be left `None`.
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Self {
SafeAccount {
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Result<Self, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now errors for no good reason, afaict it will never fail. Can we please get the proper return type back?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will fail for example when the password is incorrect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I went through the code and propose a following change:

/// Create a new `SafeAccount` from the given `json`; if it was read from a
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
/// can be left `None`.
/// In case `password` is provided, we will attempt to read the secret from the keyfile and derive the address from it instead of reading it directly.
/// Providing password is required for `json::KeyFile`s with no address.
pub fn from_file(json: json::KeyFile, filename: Option<String>, password: Option<String>) -> Resutl<Self, Error> {
  let crypto = Crypto::from(json.crypto);
  let address = match (password, json.address) {
     (None, None) => Err("This keystore does not contain address. You need to provide password to import it".into())?
     (None, Some(address)) => address,
     (Some(password), address) => {
         let derived_address = KeyPair::from_secret(crypto.secret(&password).map_err(|_| Error::InvalidPassword)?)?.address();
         match address {
            Some(address) if address != derived_address => {
              warn!("Detected address mismatch when opening an account. Derived: {}, got: {}", derived_address, address);
            },
            _ => {},
         }
         derived_address
     }
  };

  Ok(SafeAccount {
      id: json.id.into(),
 	  version: json.version.into(),
 	  address,
 	  crypto,
 	  filename,
 	  name: json.name.unwrap_or(String::new()),
 	  meta: json.meta.unwrap_or("{}".to_owned()),
  })
}

So that all callers are required to explicitly set password to None and be aware of the failure (in case json::KeyFile does not contain an address).
(We have 3 occurences of that code)

What do you think?

}

/// Same as [`from_file`](#method.from_file), but with `password` which enables reading keystore files w/o address.
pub fn from_file_with_password(json: json::KeyFile, filename: Option<String>, password: &Option<Password>) -> Result<Self, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make password mandatory here (see above comment). The decision which function to use and what errors to handle can be left to the caller.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, it can be done, but for price of code repetition, because I use it to implement from_file

Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with the responses, proposed just one change to SafeAccount so that the caller knows under what circumstances he can expect failures.

@@ -77,16 +77,32 @@ impl SafeAccount {
/// Create a new `SafeAccount` from the given `json`; if it was read from a
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
/// can be left `None`.
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Self {
SafeAccount {
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Result<Self, Error> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I went through the code and propose a following change:

/// Create a new `SafeAccount` from the given `json`; if it was read from a
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
/// can be left `None`.
/// In case `password` is provided, we will attempt to read the secret from the keyfile and derive the address from it instead of reading it directly.
/// Providing password is required for `json::KeyFile`s with no address.
pub fn from_file(json: json::KeyFile, filename: Option<String>, password: Option<String>) -> Resutl<Self, Error> {
  let crypto = Crypto::from(json.crypto);
  let address = match (password, json.address) {
     (None, None) => Err("This keystore does not contain address. You need to provide password to import it".into())?
     (None, Some(address)) => address,
     (Some(password), address) => {
         let derived_address = KeyPair::from_secret(crypto.secret(&password).map_err(|_| Error::InvalidPassword)?)?.address();
         match address {
            Some(address) if address != derived_address => {
              warn!("Detected address mismatch when opening an account. Derived: {}, got: {}", derived_address, address);
            },
            _ => {},
         }
         derived_address
     }
  };

  Ok(SafeAccount {
      id: json.id.into(),
 	  version: json.version.into(),
 	  address,
 	  crypto,
 	  filename,
 	  name: json.name.unwrap_or(String::new()),
 	  meta: json.meta.unwrap_or("{}".to_owned()),
  })
}

So that all callers are required to explicitly set password to None and be aware of the failure (in case json::KeyFile does not contain an address).
(We have 3 occurences of that code)

What do you think?


impl RootDiskDirectory {
pub fn create<P>(path: P) -> Result<Self, Error> where P: AsRef<Path> {
fs::create_dir_all(&path)?;
Ok(Self::at(path))
}

/// allows to read keyfiles with given password (needed for keyfiles w/o address)
pub fn with_password(&self, password: Option<Password>) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have import-wallet, but it seems it only works for presale wallets currently. I'd be more inclined to handle it there than to hack around DiskKeyFileManager (since the former command allows importing directly to a vault, etc).

That said I'm fine with this if you address the SafeAccount semantics in a way I proposed in the other comment.

@tworec
Copy link
Contributor Author

tworec commented Jan 2, 2019

@tomusdrw you're suggestion is incorporated.
@niklasad1 can you pls approve?

@niklasad1 niklasad1 added A7-looksgoodtestsfail 🤖 Pull request is reviewed well, but cannot be merged due to tests failing. and removed A8-looksgood 🦄 Pull request is reviewed well. labels Jan 3, 2019
@tworec
Copy link
Contributor Author

tworec commented Jan 3, 2019

pushing merge to retrigger builds (tests are passing for me)

@niklasad1
Copy link
Collaborator

@tworec Thank you very much sorry for delayed response/review.

I don't know really understand why the tests fail seems unrelated to this PR (stratum)

Perhaps merge/rebase will fix it?

/cc @tomusdrw any idea?

@niklasad1 niklasad1 added A8-looksgood 🦄 Pull request is reviewed well. and removed A7-looksgoodtestsfail 🤖 Pull request is reviewed well, but cannot be merged due to tests failing. labels Jan 3, 2019
@niklasad1 niklasad1 merged commit 469f9c2 into openethereum:master Jan 3, 2019
@tworec tworec deleted the pyethereum-keystore-support branch January 3, 2019 15:21
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A8-looksgood 🦄 Pull request is reviewed well. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants