cfxstore/accounts_dir/
vault.rs

1// Copyright 2015-2019 Parity Technologies (UK) Ltd.
2// This file is part of Parity Ethereum.
3
4// Parity Ethereum is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Ethereum is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Ethereum.  If not, see <http://www.gnu.org/licenses/>.
16
17use super::{
18    super::account::Crypto,
19    disk::{self, DiskDirectory, KeyFileManager},
20    KeyDirectory, SetKeyError, VaultKey, VaultKeyDirectory,
21};
22use crate::{json, Error, SafeAccount};
23use cfx_crypto::crypto::keccak::Keccak256;
24use parking_lot::Mutex;
25use std::{
26    fs, io,
27    path::{Path, PathBuf},
28};
29
30/// Name of vault metadata file
31pub const VAULT_FILE_NAME: &str = "vault.json";
32/// Name of temporary vault metadata file
33pub const VAULT_TEMP_FILE_NAME: &str = "vault_temp.json";
34
35/// Vault directory implementation
36pub type VaultDiskDirectory = DiskDirectory<VaultKeyFileManager>;
37
38/// Vault key file manager
39pub struct VaultKeyFileManager {
40    name: String,
41    key: VaultKey,
42    meta: Mutex<String>,
43}
44
45impl VaultDiskDirectory {
46    /// Create new vault directory with given key
47    pub fn create<P>(
48        root: P, name: &str, key: VaultKey,
49    ) -> Result<Self, Error>
50    where P: AsRef<Path> {
51        // check that vault directory does not exists
52        let vault_dir_path = make_vault_dir_path(root, name, true)?;
53        if vault_dir_path.exists() {
54            return Err(Error::CreationFailed);
55        }
56
57        // create vault && vault file
58        let vault_meta = "{}";
59        fs::create_dir_all(&vault_dir_path)?;
60        if let Err(err) = create_vault_file(&vault_dir_path, &key, vault_meta) {
61            let _ = fs::remove_dir_all(&vault_dir_path); // can't do anything with this
62            return Err(err);
63        }
64
65        Ok(DiskDirectory::new(
66            vault_dir_path,
67            VaultKeyFileManager::new(name, key, vault_meta),
68        ))
69    }
70
71    /// Open existing vault directory with given key
72    pub fn at<P>(root: P, name: &str, key: VaultKey) -> Result<Self, Error>
73    where P: AsRef<Path> {
74        // check that vault directory exists
75        let vault_dir_path = make_vault_dir_path(root, name, true)?;
76        if !vault_dir_path.is_dir() {
77            return Err(Error::CreationFailed);
78        }
79
80        // check that passed key matches vault file
81        let meta = read_vault_file(&vault_dir_path, Some(&key))?;
82
83        Ok(DiskDirectory::new(
84            vault_dir_path,
85            VaultKeyFileManager::new(name, key, &meta),
86        ))
87    }
88
89    /// Read vault meta without actually opening the vault
90    pub fn meta_at<P>(root: P, name: &str) -> Result<String, Error>
91    where P: AsRef<Path> {
92        // check that vault directory exists
93        let vault_dir_path = make_vault_dir_path(root, name, true)?;
94        if !vault_dir_path.is_dir() {
95            return Err(Error::VaultNotFound);
96        }
97
98        // check that passed key matches vault file
99        read_vault_file(&vault_dir_path, None)
100    }
101
102    fn create_temp_vault(
103        &self, key: VaultKey,
104    ) -> Result<VaultDiskDirectory, Error> {
105        let original_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed");
106        let mut path: PathBuf = original_path.clone();
107        let name = self.name();
108
109        path.push(name); // to jump to the next level
110
111        let mut index = 0;
112        loop {
113            let name = format!("{}_temp_{}", name, index);
114            path.set_file_name(&name);
115            if !path.exists() {
116                return VaultDiskDirectory::create(original_path, &name, key);
117            }
118
119            index += 1;
120        }
121    }
122
123    fn copy_to_vault(&self, vault: &VaultDiskDirectory) -> Result<(), Error> {
124        for account in self.load()? {
125            let filename = account.filename.clone().expect("self is instance of DiskDirectory; DiskDirectory fills filename in load; qed");
126            vault.insert_with_filename(account, filename, true)?;
127        }
128
129        Ok(())
130    }
131
132    fn delete(&self) -> Result<(), Error> {
133        let path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed");
134        fs::remove_dir_all(path).map_err(Into::into)
135    }
136}
137
138impl VaultKeyDirectory for VaultDiskDirectory {
139    fn as_key_directory(&self) -> &dyn KeyDirectory { self }
140
141    fn name(&self) -> &str { &self.key_manager().name }
142
143    fn key(&self) -> VaultKey { self.key_manager().key.clone() }
144
145    fn set_key(&self, new_key: VaultKey) -> Result<(), SetKeyError> {
146        let temp_vault = VaultDiskDirectory::create_temp_vault(self, new_key)
147            .map_err(SetKeyError::NonFatalOld)?;
148        let mut source_path = temp_vault.path().expect("temp_vault is instance of DiskDirectory; DiskDirectory always returns path; qed").clone();
149        let mut target_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed").clone();
150
151        // preserve meta
152        temp_vault
153            .set_meta(&self.meta())
154            .map_err(SetKeyError::NonFatalOld)?;
155
156        // jump to next fs level
157        source_path.push("next");
158        target_path.push("next");
159
160        let temp_accounts = self
161            .copy_to_vault(&temp_vault)
162            .and_then(|_| temp_vault.load())
163            .map_err(|err| {
164                // ignore error, as we already processing error
165                let _ = temp_vault.delete();
166                SetKeyError::NonFatalOld(err)
167            })?;
168
169        // we can't just delete temp vault until all files moved, because
170        // original vault content has already been partially replaced
171        // => when error or crash happens here, we can't do anything
172        for temp_account in temp_accounts {
173            let filename = temp_account.filename.expect("self is instance of DiskDirectory; DiskDirectory fills filename in load; qed");
174            source_path.set_file_name(&filename);
175            target_path.set_file_name(&filename);
176            fs::rename(&source_path, &target_path)
177                .map_err(|err| SetKeyError::Fatal(err.into()))?;
178        }
179        source_path.set_file_name(VAULT_FILE_NAME);
180        target_path.set_file_name(VAULT_FILE_NAME);
181        fs::rename(source_path, target_path)
182            .map_err(|err| SetKeyError::Fatal(err.into()))?;
183
184        temp_vault.delete().map_err(SetKeyError::NonFatalNew)
185    }
186
187    fn meta(&self) -> String { self.key_manager().meta.lock().clone() }
188
189    fn set_meta(&self, meta: &str) -> Result<(), Error> {
190        let key_manager = self.key_manager();
191        let vault_path = self.path().expect("self is instance of DiskDirectory; DiskDirectory always returns path; qed");
192        create_vault_file(vault_path, &key_manager.key, meta)?;
193        *key_manager.meta.lock() = meta.to_owned();
194        Ok(())
195    }
196}
197
198impl VaultKeyFileManager {
199    pub fn new(name: &str, key: VaultKey, meta: &str) -> Self {
200        VaultKeyFileManager {
201            name: name.into(),
202            key,
203            meta: Mutex::new(meta.to_owned()),
204        }
205    }
206}
207
208impl KeyFileManager for VaultKeyFileManager {
209    fn read<T>(
210        &self, filename: Option<String>, reader: T,
211    ) -> Result<SafeAccount, Error>
212    where T: io::Read {
213        let vault_file = json::VaultKeyFile::load(reader)
214            .map_err(|e| Error::Custom(format!("{:?}", e)))?;
215        let mut safe_account = SafeAccount::from_vault_file(
216            &self.key.password,
217            vault_file,
218            filename,
219        )?;
220
221        safe_account.meta = json::insert_vault_name_to_json_meta(
222            &safe_account.meta,
223            &self.name,
224        )
225        .map_err(|err| Error::Custom(format!("{:?}", err)))?;
226        Ok(safe_account)
227    }
228
229    fn write<T>(
230        &self, mut account: SafeAccount, writer: &mut T,
231    ) -> Result<(), Error>
232    where T: io::Write {
233        account.meta = json::remove_vault_name_from_json_meta(&account.meta)
234            .map_err(|err| Error::Custom(format!("{:?}", err)))?;
235
236        let vault_file: json::VaultKeyFile =
237            account.into_vault_file(self.key.iterations, &self.key.password)?;
238        vault_file
239            .write(writer)
240            .map_err(|e| Error::Custom(format!("{:?}", e)))
241    }
242}
243
244/// Makes path to vault directory, checking that vault name is appropriate
245fn make_vault_dir_path<P>(
246    root: P, name: &str, check_name: bool,
247) -> Result<PathBuf, Error>
248where P: AsRef<Path> {
249    // check vault name
250    if check_name && !check_vault_name(name) {
251        return Err(Error::InvalidVaultName);
252    }
253
254    let mut vault_dir_path: PathBuf = root.as_ref().into();
255    vault_dir_path.push(name);
256    Ok(vault_dir_path)
257}
258
259/// Every vault must have unique name => we rely on filesystem to check this
260/// => vault name must not contain any fs-special characters to avoid directory
261/// traversal => we only allow alphanumeric + separator characters in vault
262/// name.
263fn check_vault_name(name: &str) -> bool {
264    !name.is_empty()
265        && name.chars().all(|c| {
266            c.is_alphanumeric() || c.is_whitespace() || c == '-' || c == '_'
267        })
268}
269
270/// Vault can be empty, but still must be pluggable => we store vault password
271/// in separate file
272fn create_vault_file<P>(
273    vault_dir_path: P, key: &VaultKey, meta: &str,
274) -> Result<(), Error>
275where P: AsRef<Path> {
276    let password_hash = key.password.as_bytes().keccak256();
277    let crypto =
278        Crypto::with_plain(&password_hash, &key.password, key.iterations)?;
279
280    let vault_file_path = vault_dir_path.as_ref().join(VAULT_FILE_NAME);
281    let temp_vault_file_name = disk::find_unique_filename_using_random_suffix(
282        vault_dir_path.as_ref(),
283        &VAULT_TEMP_FILE_NAME,
284    )?;
285    let temp_vault_file_path =
286        vault_dir_path.as_ref().join(&temp_vault_file_name);
287
288    // this method is used to rewrite existing vault file
289    // => write to temporary file first, then rename temporary file to vault
290    // file
291    let mut vault_file =
292        disk::create_new_file_with_permissions_to_owner(&temp_vault_file_path)?;
293    let vault_file_contents = json::VaultFile {
294        crypto: crypto.into(),
295        meta: Some(meta.to_owned()),
296    };
297    vault_file_contents
298        .write(&mut vault_file)
299        .map_err(|e| Error::Custom(format!("{:?}", e)))?;
300    drop(vault_file);
301    fs::rename(&temp_vault_file_path, &vault_file_path)?;
302
303    Ok(())
304}
305
306/// When vault is opened => we must check that password matches && read metadata
307fn read_vault_file<P>(
308    vault_dir_path: P, key: Option<&VaultKey>,
309) -> Result<String, Error>
310where P: AsRef<Path> {
311    let mut vault_file_path: PathBuf = vault_dir_path.as_ref().into();
312    vault_file_path.push(VAULT_FILE_NAME);
313
314    let vault_file = fs::File::open(vault_file_path)?;
315    let vault_file_contents = json::VaultFile::load(vault_file)
316        .map_err(|e| Error::Custom(format!("{:?}", e)))?;
317    let vault_file_meta = vault_file_contents.meta.unwrap_or("{}".to_owned());
318    let vault_file_crypto: Crypto = vault_file_contents.crypto.into();
319
320    if let Some(key) = key {
321        let password_bytes = vault_file_crypto.decrypt(&key.password)?;
322        let password_hash = key.password.as_bytes().keccak256();
323        if password_hash != password_bytes.as_slice() {
324            return Err(Error::InvalidPassword);
325        }
326    }
327
328    Ok(vault_file_meta)
329}
330
331#[cfg(test)]
332mod test {
333    use super::{
334        check_vault_name, create_vault_file, make_vault_dir_path,
335        read_vault_file, VaultDiskDirectory, VaultKey, VAULT_FILE_NAME,
336    };
337    use std::{fs, io::Write, path::PathBuf};
338    use tempfile::tempdir;
339
340    #[test]
341    fn check_vault_name_succeeds() {
342        assert!(check_vault_name("vault"));
343        assert!(check_vault_name("vault with spaces"));
344        assert!(check_vault_name("vault	with	tabs"));
345        assert!(check_vault_name("vault_with_underscores"));
346        assert!(check_vault_name("vault-with-dashes"));
347        assert!(check_vault_name("vault-with-digits-123"));
348        assert!(check_vault_name("vault中文名字"));
349    }
350
351    #[test]
352    fn check_vault_name_fails() {
353        assert!(!check_vault_name(""));
354        assert!(!check_vault_name("."));
355        assert!(!check_vault_name("*"));
356        assert!(!check_vault_name("../.bash_history"));
357        assert!(!check_vault_name("/etc/passwd"));
358        assert!(!check_vault_name("c:\\windows"));
359    }
360
361    #[test]
362    fn make_vault_dir_path_succeeds() {
363        use std::path::Path;
364
365        assert_eq!(
366            &make_vault_dir_path("/home/user/parity", "vault", true).unwrap(),
367            &Path::new("/home/user/parity/vault")
368        );
369        assert_eq!(
370            &make_vault_dir_path("/home/user/parity", "*bad-name*", false)
371                .unwrap(),
372            &Path::new("/home/user/parity/*bad-name*")
373        );
374    }
375
376    #[test]
377    fn make_vault_dir_path_fails() {
378        assert!(make_vault_dir_path("/home/user/parity", "*bad-name*", true)
379            .is_err());
380    }
381
382    #[test]
383    fn create_vault_file_succeeds() {
384        // given
385        let temp_path = tempdir().unwrap();
386        let key = VaultKey::new(&"password".into(), 1024);
387        let mut vault_dir: PathBuf = temp_path.path().into();
388        vault_dir.push("vault");
389        fs::create_dir_all(&vault_dir).unwrap();
390
391        // when
392        let result = create_vault_file(&vault_dir, &key, "{}");
393
394        // then
395        assert!(result.is_ok());
396        let mut vault_file_path = vault_dir;
397        vault_file_path.push(VAULT_FILE_NAME);
398        assert!(vault_file_path.exists() && vault_file_path.is_file());
399    }
400
401    #[test]
402    fn read_vault_file_succeeds() {
403        // given
404        let temp_path = tempdir().unwrap();
405        let key = VaultKey::new(&"password".into(), 1024);
406        let vault_file_contents = r#"{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"758696c8dc6378ab9b25bb42790da2f5"},"ciphertext":"54eb50683717d41caaeb12ea969f2c159daada5907383f26f327606a37dc7168","kdf":"pbkdf2","kdfparams":{"c":1024,"dklen":32,"prf":"hmac-sha256","salt":"3c320fa566a1a7963ac8df68a19548d27c8f40bf92ef87c84594dcd5bbc402b6"},"mac":"9e5c2314c2a0781962db85611417c614bd6756666b6b1e93840f5b6ed895f003"}}"#;
407        let dir: PathBuf = temp_path.path().into();
408        let mut vault_file_path: PathBuf = dir.clone();
409        vault_file_path.push(VAULT_FILE_NAME);
410        {
411            let mut vault_file = fs::File::create(vault_file_path).unwrap();
412            vault_file
413                .write_all(vault_file_contents.as_bytes())
414                .unwrap();
415        }
416
417        // when
418        let result = read_vault_file(&dir, Some(&key));
419
420        // then
421        assert!(result.is_ok());
422    }
423
424    #[test]
425    fn read_vault_file_fails() {
426        // given
427        let temp_path = tempdir().unwrap();
428        let key = VaultKey::new(&"password1".into(), 1024);
429        let dir: PathBuf = temp_path.path().into();
430        let mut vault_file_path: PathBuf = dir.clone();
431        vault_file_path.push(VAULT_FILE_NAME);
432
433        // when
434        let result = read_vault_file(&dir, Some(&key));
435
436        // then
437        assert!(result.is_err());
438
439        // and when given
440        let vault_file_contents = r#"{"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"0155e3690be19fbfbecabcd440aa284b"},"ciphertext":"4d6938a1f49b7782","kdf":"pbkdf2","kdfparams":{"c":1024,"dklen":32,"prf":"hmac-sha256","salt":"b6a9338a7ccd39288a86dba73bfecd9101b4f3db9c9830e7c76afdbd4f6872e5"},"mac":"16381463ea11c6eb2239a9f339c2e780516d29d234ce30ac5f166f9080b5a262"}}"#;
441        {
442            let mut vault_file = fs::File::create(vault_file_path).unwrap();
443            vault_file
444                .write_all(vault_file_contents.as_bytes())
445                .unwrap();
446        }
447
448        // when
449        let result = read_vault_file(&dir, Some(&key));
450
451        // then
452        assert!(result.is_err());
453    }
454
455    #[test]
456    fn vault_directory_can_be_created() {
457        // given
458        let temp_path = tempdir().unwrap();
459        let key = VaultKey::new(&"password".into(), 1024);
460        let dir: PathBuf = temp_path.path().into();
461
462        // when
463        let vault = VaultDiskDirectory::create(&dir, "vault", key.clone());
464
465        // then
466        assert!(vault.is_ok());
467
468        // and when
469        let vault = VaultDiskDirectory::at(&dir, "vault", key);
470
471        // then
472        assert!(vault.is_ok());
473    }
474
475    #[test]
476    fn vault_directory_cannot_be_created_if_already_exists() {
477        // given
478        let temp_path = tempdir().unwrap();
479        let key = VaultKey::new(&"password".into(), 1024);
480        let dir: PathBuf = temp_path.path().into();
481        let mut vault_dir = dir.clone();
482        vault_dir.push("vault");
483        fs::create_dir_all(&vault_dir).unwrap();
484
485        // when
486        let vault = VaultDiskDirectory::create(&dir, "vault", key);
487
488        // then
489        assert!(vault.is_err());
490    }
491
492    #[test]
493    fn vault_directory_cannot_be_opened_if_not_exists() {
494        // given
495        let temp_path = tempdir().unwrap();
496        let key = VaultKey::new(&"password".into(), 1024);
497        let dir: PathBuf = temp_path.path().into();
498
499        // when
500        let vault = VaultDiskDirectory::at(&dir, "vault", key);
501
502        // then
503        assert!(vault.is_err());
504    }
505}