cfxstore/account/
safe_account.rs

1// Copyright 2019 Conflux Foundation. All rights reserved.
2// Conflux is free software and distributed under GNU General Public License.
3// See http://www.gnu.org/licenses/
4
5// Copyright 2015-2019 Parity Technologies (UK) Ltd.
6// This file is part of Parity Ethereum.
7
8// Parity Ethereum is free software: you can redistribute it and/or modify
9// it under the terms of the GNU General Public License as published by
10// the Free Software Foundation, either version 3 of the License, or
11// (at your option) any later version.
12
13// Parity Ethereum is distributed in the hope that it will be useful,
14// but WITHOUT ANY WARRANTY; without even the implied warranty of
15// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16// GNU General Public License for more details.
17
18// You should have received a copy of the GNU General Public License
19// along with Parity Ethereum.  If not, see <http://www.gnu.org/licenses/>.
20
21use super::crypto::Crypto;
22use crate::{account::Version, json, Error};
23use cfx_crypto::crypto::ecdh::agree;
24use cfx_types::address_util::AddressUtil;
25use cfxkey::{
26    self, sign, Address, KeyPair, Message, Password, Public, Secret, Signature,
27};
28use log::warn;
29
30/// Account representation.
31#[derive(Debug, PartialEq, Clone)]
32pub struct SafeAccount {
33    /// Account ID
34    pub id: [u8; 16],
35    /// Account version
36    pub version: Version,
37    /// Account address
38    pub address: Address,
39    /// Account private key derivation definition.
40    pub crypto: Crypto,
41    /// Account filename
42    pub filename: Option<String>,
43    /// Account name
44    pub name: String,
45    /// Account metadata
46    pub meta: String,
47}
48
49impl Into<json::KeyFile> for SafeAccount {
50    fn into(self) -> json::KeyFile {
51        json::KeyFile {
52            id: From::from(self.id),
53            version: self.version.into(),
54            address: Some(self.address.into()),
55            crypto: self.crypto.into(),
56            name: Some(self.name),
57            meta: Some(self.meta),
58        }
59    }
60}
61
62impl SafeAccount {
63    /// Create a new account
64    pub fn create(
65        keypair: &KeyPair, id: [u8; 16], password: &Password, iterations: u32,
66        name: String, meta: String,
67    ) -> Result<Self, Error> {
68        Ok(SafeAccount {
69            id,
70            version: Version::V3,
71            crypto: Crypto::with_secret(
72                keypair.secret(),
73                password,
74                iterations,
75            )?,
76            address: keypair.address(),
77            filename: None,
78            name,
79            meta,
80        })
81    }
82
83    /// Create a new `SafeAccount` from the given `json`; if it was read from a
84    /// file, the `filename` should be `Some` name. If it is as yet anonymous,
85    /// then it can be left `None`.
86    /// In case `password` is provided, we will attempt to read the secret from
87    /// the keyfile and derive the address from it instead of reading it
88    /// directly. Providing password is required for `json::KeyFile`s with
89    /// no address.
90    pub fn from_file(
91        json: json::KeyFile, filename: Option<String>,
92        password: &Option<Password>,
93    ) -> Result<Self, Error> {
94        let crypto = Crypto::from(json.crypto);
95        let address = match (password, &json.address) {
96			(None, Some(json_address)) => json_address.into(),
97			(None, None) => return Err(Error::Custom(
98				"This keystore does not contain address. You need to provide password to import it".into())),
99			(Some(password), json_address) => {
100				let derived_address = KeyPair::from_secret(
101					crypto.secret(&password).map_err(|_| Error::InvalidPassword)?
102				)?.address();
103
104				match json_address {
105					Some(json_address) => {
106						let json_address = json_address.into();
107						if derived_address != json_address {
108                            warn!("Detected address mismatch when opening an account. Derived: {:?}, in json got: {:?}. Are you trying to import an Ethkey for Conflux? Note that the address scheme between Ethereum and Conflux are different.", derived_address, json_address);
109                            return Err(Error::Custom(format!("Address mismatch. Derived: {:?}, in json got: {:?}.", derived_address, json_address)));
110						}
111					},
112					_ => {},
113				}
114				derived_address
115			}
116		};
117
118        Ok(SafeAccount {
119            id: json.id.into(),
120            version: json.version.into(),
121            address,
122            crypto,
123            filename,
124            name: json.name.unwrap_or_default(),
125            meta: json.meta.unwrap_or("{}".to_owned()),
126        })
127    }
128
129    /// Create a new `SafeAccount` from the given vault `json`; if it was read
130    /// from a file, the `filename` should be `Some` name. If it is as yet
131    /// anonymous, then it can be left `None`.
132    pub fn from_vault_file(
133        password: &Password, json: json::VaultKeyFile, filename: Option<String>,
134    ) -> Result<Self, Error> {
135        let meta_crypto: Crypto = json.metacrypto.into();
136        let meta_plain = meta_crypto.decrypt(password)?;
137        let meta_plain = json::VaultKeyMeta::load(&meta_plain)
138            .map_err(|e| Error::Custom(format!("{:?}", e)))?;
139
140        if !Address::from_slice(&meta_plain.address).is_user_account_address() {
141            warn!("Trying to import a non-user type account address. Are you trying to import an Ethkey for Conflux? Note that the address scheme between Ethereum and Conflux are different.");
142            return Err(Error::Custom(format!(
143                "Import non-user type address. Address: {:?}",
144                meta_plain.address
145            )));
146        }
147
148        SafeAccount::from_file(
149            json::KeyFile {
150                id: json.id,
151                version: json.version,
152                crypto: json.crypto,
153                address: Some(meta_plain.address),
154                name: meta_plain.name,
155                meta: meta_plain.meta,
156            },
157            filename,
158            &None,
159        )
160    }
161
162    /// Create a new `VaultKeyFile` from the given `self`
163    pub fn into_vault_file(
164        self, iterations: u32, password: &Password,
165    ) -> Result<json::VaultKeyFile, Error> {
166        let meta_plain = json::VaultKeyMeta {
167            address: self.address.into(),
168            name: Some(self.name),
169            meta: Some(self.meta),
170        };
171        let meta_plain = meta_plain
172            .write()
173            .map_err(|e| Error::Custom(format!("{:?}", e)))?;
174        let meta_crypto =
175            Crypto::with_plain(&meta_plain, password, iterations)?;
176
177        Ok(json::VaultKeyFile {
178            id: self.id.into(),
179            version: self.version.into(),
180            crypto: self.crypto.into(),
181            metacrypto: meta_crypto.into(),
182        })
183    }
184
185    /// Sign a message.
186    pub fn sign(
187        &self, password: &Password, message: &Message,
188    ) -> Result<Signature, Error> {
189        let secret = self.crypto.secret(password)?;
190        sign(&secret, message).map_err(From::from)
191    }
192
193    /// Decrypt a message.
194    pub fn decrypt(
195        &self, password: &Password, shared_mac: &[u8], message: &[u8],
196    ) -> Result<Vec<u8>, Error> {
197        let secret = self.crypto.secret(password)?;
198        cfx_crypto::crypto::ecies::decrypt(&secret, shared_mac, message)
199            .map_err(From::from)
200    }
201
202    /// Agree on shared key.
203    pub fn agree(
204        &self, password: &Password, other: &Public,
205    ) -> Result<Secret, Error> {
206        let secret = self.crypto.secret(password)?;
207        agree(&secret, other).map_err(From::from)
208    }
209
210    /// Derive public key.
211    pub fn public(&self, password: &Password) -> Result<Public, Error> {
212        let secret = self.crypto.secret(password)?;
213        Ok(KeyPair::from_secret(secret)?.public().clone())
214    }
215
216    /// Change account's password.
217    pub fn change_password(
218        &self, old_password: &Password, new_password: &Password,
219        iterations: u32,
220    ) -> Result<Self, Error> {
221        let secret = self.crypto.secret(old_password)?;
222        let result = SafeAccount {
223            id: self.id.clone(),
224            version: self.version.clone(),
225            crypto: Crypto::with_secret(&secret, new_password, iterations)?,
226            address: self.address.clone(),
227            filename: self.filename.clone(),
228            name: self.name.clone(),
229            meta: self.meta.clone(),
230        };
231        Ok(result)
232    }
233
234    /// Check if password matches the account.
235    pub fn check_password(&self, password: &Password) -> bool {
236        self.crypto.secret(password).is_ok()
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::SafeAccount;
243    use cfxkey::{verify_public, Generator, Message, Random};
244
245    #[test]
246    fn sign_and_verify_public() {
247        let keypair = Random.generate().unwrap();
248        let password = "hello world".into();
249        let message = Message::default();
250        let account = SafeAccount::create(
251            &keypair,
252            [0u8; 16],
253            &password,
254            10240,
255            "Test".to_owned(),
256            "{}".to_owned(),
257        );
258        let signature = account.unwrap().sign(&password, &message).unwrap();
259        assert!(verify_public(keypair.public(), &signature, &message).unwrap());
260    }
261
262    #[test]
263    fn change_password() {
264        let keypair = Random.generate().unwrap();
265        let first_password = "hello world".into();
266        let sec_password = "this is sparta".into();
267        let i = 10240;
268        let message = Message::default();
269        let account = SafeAccount::create(
270            &keypair,
271            [0u8; 16],
272            &first_password,
273            i,
274            "Test".to_owned(),
275            "{}".to_owned(),
276        )
277        .unwrap();
278        let new_account = account
279            .change_password(&first_password, &sec_password, i)
280            .unwrap();
281        assert!(account.sign(&first_password, &message).is_ok());
282        assert!(account.sign(&sec_password, &message).is_err());
283        assert!(new_account.sign(&first_password, &message).is_err());
284        assert!(new_account.sign(&sec_password, &message).is_ok());
285    }
286}