-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Pyethereum keystore support #9710
Pyethereum keystore support #9710
Conversation
It looks like this contributor signed our Contributor License Agreement. 👍 Many thanks, Parity Technologies CLA Bot |
There was a problem hiding this 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
ethstore/src/account/safe_account.rs
Outdated
let crypto = Crypto::from(json.crypto); | ||
let address = match json.address { | ||
Some(address) => address.into(), | ||
None => match password { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Are you still working on this @tworec |
I want to finish this. I was looking to see @debris answers... |
Co-Authored-By: tworec <[email protected]>
Co-Authored-By: tworec <[email protected]>
👍 needs a 2nd review |
There was a problem hiding this 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.");
@niklasad1 done, but I've initialized log level to This is sample CLI output with invalid password
|
@tworec could you rebase this? @niklasad1 could you review this again? |
@5chdn I think rebase is overkill here. I've merged instead. |
yes, whatever you prefer :) |
So why do we need a review? Merge introduced no changes in this PR |
yes. one more review. please wait till this holiday craze is over. |
ok
…On Wed, 2 Jan 2019 at 10:50, Afri Schoedon ***@***.***> wrote:
yes. one more review. please wait till this holiday craze is over.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#9710 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AMIYUW4O4D5r9oH2PF_YNrYD8mJPNaHgks5u_IDagaJpZM4XLCJZ>
.
|
There was a problem hiding this 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") |
There was a problem hiding this comment.
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
)
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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:
- The assumption is that all accounts inside the directory share the same password which is most likely incorrect.
- 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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
@tomusdrw you're suggestion is incorporated. |
pushing merge to retrigger builds (tests are passing for me) |
Continuation of #9444
(sorry for duplicate, but I can not reopen #9444)