From fc92c4ec71ae90130492289551a2c552fda00d22 Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 10 Dec 2021 21:39:40 +0800 Subject: [PATCH] update keystore --- Cargo.toml | 1 + lib/pages/account_generate.dart | 2 +- lib/pages/account_restore.dart | 2 +- lib/pages/setting/profile.dart | 4 +- lib/security.dart | 4 +- lib/widgets/show_pin.dart | 72 ++++++++++++-------- pubspec.yaml | 1 - src/account.rs | 114 +++++++++++--------------------- src/apps/wallet/models.rs | 11 ++- src/apps/wallet/rpc.rs | 11 +-- src/group.rs | 12 ++++ src/rpc.rs | 11 ++- src/utils.rs | 1 + src/utils/crypto.rs | 107 ++++++++++++++++++++++++++++++ 14 files changed, 232 insertions(+), 121 deletions(-) create mode 100644 src/utils/crypto.rs diff --git a/Cargo.toml b/Cargo.toml index a730783..011e783 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ once_cell = "1.8" simplelog = "0.11" image = "0.23" base64 = "0.13" +hex = "0.4" sha2 = "0.10" blake3 = "1.2" bincode = "1.3" diff --git a/lib/pages/account_generate.dart b/lib/pages/account_generate.dart index 370dd65..52c4057 100644 --- a/lib/pages/account_generate.dart +++ b/lib/pages/account_generate.dart @@ -94,7 +94,7 @@ class _AccountGeneratePageState extends State { Icons.security_rounded, title, SetPinWords( - callback: (key, lock) async { + callback: (lock) async { Navigator.of(context).pop(); // send to core node service by rpc. final res = await httpPost(Global.httpRpc, diff --git a/lib/pages/account_restore.dart b/lib/pages/account_restore.dart index 1d6639b..e34d06b 100644 --- a/lib/pages/account_restore.dart +++ b/lib/pages/account_restore.dart @@ -401,7 +401,7 @@ class _AccountRestorePageState extends State { Icons.security_rounded, title, SetPinWords( - callback: (key, lock) async { + callback: (lock) async { Navigator.of(context).pop(); // send to core node service by rpc. final res = await httpPost(Global.httpRpc, 'account-restore', [ diff --git a/lib/pages/setting/profile.dart b/lib/pages/setting/profile.dart index 198c7f6..9710ade 100644 --- a/lib/pages/setting/profile.dart +++ b/lib/pages/setting/profile.dart @@ -250,7 +250,7 @@ class _ProfileDetailState extends State { context, Icons.security_rounded, title, - SetPinWords(callback: (key, lock2) async { + SetPinWords(callback: (lock2) async { Navigator.of(context).pop(); final res = await httpPost(Global.httpRpc, 'account-pin', [lock, lock2]); if (res.isOk) { @@ -286,7 +286,7 @@ class _ProfileDetailState extends State { title, PinWords( hashPin: hash, - callback: (_key, _hash) async { + callback: (_) async { Navigator.of(context).pop(); callback(); })); diff --git a/lib/security.dart b/lib/security.dart index 5ea3f39..f8e1da1 100644 --- a/lib/security.dart +++ b/lib/security.dart @@ -255,9 +255,9 @@ class _SecurityPageState extends State { title, PinWords( hashPin: this._selectedUserLock, - callback: (pinWords, lock) async { + callback: (pinWords) async { Navigator.of(context).pop(); - _verifyAfter(lock); + _verifyAfter(pinWords); })); } } diff --git a/lib/widgets/show_pin.dart b/lib/widgets/show_pin.dart index ff0f076..a0261da 100644 --- a/lib/widgets/show_pin.dart +++ b/lib/widgets/show_pin.dart @@ -1,9 +1,9 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator; import 'package:esse/l10n/localizations.dart'; +import 'package:esse/rpc.dart'; +import 'package:esse/global.dart'; const pinLength = 6; @@ -66,27 +66,45 @@ class PinWords extends StatefulWidget { } class _PinWordsState extends State { - String pinWords = ''; - bool isError = false; + String _pinWords = ''; + bool _isError = false; + bool _waiting = false; + + _checkPin() async { + bool check = false; + final res = await httpPost(Global.httpRpc, 'account-pin-check', [_pinWords]); + if (res.isOk) { + check = res.params[0]; + } else { + print(res.error); + } + + if (widget.hashPin != "" && !check) { + setState(() { + _waiting = false; + _pinWords = ''; + _isError = true; + }); + } else { + setState(() {}); + widget.callback(_pinWords); + } + } _inputCallback(String text) { - isError = false; - pinWords += text; - if (pinWords.length < pinLength) { + if (this._waiting) { + return; + } + + _isError = false; + _pinWords += text; + if (_pinWords.length < pinLength) { setState(() {}); } else { - final bytes = utf8.encode(pinWords); - final lock = "${sha256.convert(bytes)}"; - - if (widget.hashPin != "" && widget.hashPin != lock) { - setState(() { - pinWords = ''; - isError = true; - }); - } else { - setState(() {}); - widget.callback(pinWords, lock); - } + setState(() { + this._waiting = true; + }); + _checkPin(); } } @@ -97,8 +115,8 @@ class _PinWordsState extends State { Container( margin: EdgeInsets.all(6.0), child: _circle( - isError, - i < pinWords.length, + _isError, + i < _pinWords.length, color.primary, color.primaryVariant, ), @@ -114,6 +132,7 @@ class _PinWordsState extends State { //final lang = AppLocalizations.of(context); return Column(children: [ + this._waiting ? CupertinoActivityIndicator(radius: 10.0, animating: true) : const SizedBox(), Container( margin: const EdgeInsets.only(bottom: 20.0), height: 40, @@ -141,9 +160,9 @@ class _PinWordsState extends State { _keyboradInput(color.primary, color.background, '0', _inputCallback), GestureDetector( onTap: () { - if (pinWords.length > 0) { + if (_pinWords.length > 0) { setState(() { - pinWords = pinWords.substring(0, pinWords.length - 1); + _pinWords = _pinWords.substring(0, _pinWords.length - 1); }); } }, @@ -213,10 +232,7 @@ class _SetPinWordsState extends State { this._pinWords = ''; }); } else { - setState(() {}); - final bytes = utf8.encode(this._pinWords); - final lock = "${sha256.convert(bytes)}"; - widget.callback(this._pinWords, lock); + widget.callback(this._pinWords); } } } diff --git a/pubspec.yaml b/pubspec.yaml index 9c71cae..15b643c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: url: git://github.com/google/flutter-desktop-embedding.git path: plugins/file_selector/file_selector_windows open_file: any # open file in mobile. - crypto: any crop: any unorm_dart: any qr_flutter: any diff --git a/src/account.rs b/src/account.rs index cf8c833..dff07c1 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,5 +1,3 @@ -use aes_gcm::aead::{generic_array::GenericArray, Aead, NewAead}; -use aes_gcm::Aes256Gcm; use std::time::{SystemTime, UNIX_EPOCH}; use tdn::types::{ group::{EventId, GroupId}, @@ -8,6 +6,8 @@ use tdn::types::{ use tdn_did::{generate_id, Keypair, Language}; use tdn_storage::local::{DStorage, DsValue}; +use crate::utils::crypto::{check_pin, decrypt, decrypt_multiple, encrypt_multiple, hash_pin}; + fn mnemonic_lang_to_i64(lang: Language) -> i64 { match lang { Language::English => 0, @@ -48,7 +48,7 @@ pub(crate) struct Account { pub pass: String, pub name: String, pub avatar: Vec, - pub lock: String, // hashed-key. + pub lock: Vec, // hashed-lock. pub secret: Vec, // encrypted value. pub height: u64, pub event: EventId, @@ -62,7 +62,7 @@ impl Account { lang: i64, pass: String, name: String, - lock: String, + lock: Vec, avatar: Vec, mnemonic: Vec, secret: Vec, @@ -96,7 +96,7 @@ impl Account { pub fn generate( index: u32, - skey: &[u8], // &[u8; 32] + salt: &[u8], // &[u8; 32] lang: i64, mnemonic: &str, pass: &str, @@ -112,90 +112,56 @@ impl Account { if pass.len() > 0 { Some(pass) } else { None }, )?; - let cipher = Aes256Gcm::new(GenericArray::from_slice(skey)); // 256-bit key. - let hash_nonce = blake3::hash(lock.as_bytes()); - let nonce = GenericArray::from_slice(&hash_nonce.as_bytes()[0..12]); // 96-bit key. - - let mnemonic_bytes = cipher - .encrypt(nonce, mnemonic.as_bytes()) - .map_err(|_e| anyhow!("mnemonic lock invalid."))?; - - let sk_bytes = cipher - .encrypt(nonce, sk.to_bytes().as_ref()) - .map_err(|_e| anyhow!("secret lock invalid."))?; + let mut ebytes = encrypt_multiple(salt, lock, vec![&sk.to_bytes(), mnemonic.as_bytes()])?; + let mnemonic = ebytes.pop().unwrap_or(vec![]); + let secret = ebytes.pop().unwrap_or(vec![]); + let index = index as i64; Ok(( Account::new( gid, - index as i64, + index, lang, pass.to_string(), name.to_string(), - lock.to_string(), + hash_pin(salt, lock, index), avatar, - mnemonic_bytes, - sk_bytes, + mnemonic, + secret, ), sk, )) } - pub fn _check_lock(&self, _lock: &str) -> bool { - // TODO - true + pub fn check_lock(&self, salt: &[u8], lock: &str) -> Result<()> { + if check_pin(salt, lock, self.index, &self.lock) { + Ok(()) + } else { + Err(anyhow!("lock is invalid!")) + } } - pub fn pin(&mut self, skey: &[u8], old: &str, new: &str) -> Result<()> { - self.lock = new.to_string(); - - let cipher = Aes256Gcm::new(GenericArray::from_slice(skey)); // 256-bit key. - - let hash_old_nonce = blake3::hash(old.as_bytes()); - let hash_new_nonce = blake3::hash(new.as_bytes()); - let old_nonce = GenericArray::from_slice(&hash_old_nonce.as_bytes()[0..12]); // 96-bit key. - let new_nonce = GenericArray::from_slice(&hash_new_nonce.as_bytes()[0..12]); // 96-bit key. - - let mnemonic = cipher - .decrypt(old_nonce, self.mnemonic.as_ref()) - .map_err(|_e| anyhow!("mnemonic unlock invalid."))?; - - self.mnemonic = cipher - .encrypt(new_nonce, mnemonic.as_ref()) - .map_err(|_e| anyhow!("mnemonic lock invalid."))?; - - let secret = cipher - .decrypt(old_nonce, self.secret.as_ref()) - .map_err(|_e| anyhow!("secret unlock invalid."))?; - - self.secret = cipher - .encrypt(new_nonce, secret.as_ref()) - .map_err(|_e| anyhow!("secret lock invalid."))?; + pub fn pin(&mut self, salt: &[u8], old: &str, new: &str) -> Result<()> { + self.check_lock(salt, old)?; + let pbytes = decrypt_multiple(salt, old, vec![&self.secret, &self.mnemonic])?; + let mut ebytes = encrypt_multiple(salt, new, pbytes.iter().map(|v| v.as_ref()).collect())?; + self.mnemonic = ebytes.pop().unwrap_or(vec![]); + self.secret = ebytes.pop().unwrap_or(vec![]); + self.lock = hash_pin(salt, new, self.index); Ok(()) } - pub fn mnemonic(&self, skey: &[u8], lock: &str) -> Result { - let cipher = Aes256Gcm::new(GenericArray::from_slice(skey)); // 256-bit key. - let hash_nonce = blake3::hash(lock.as_bytes()); - let nonce = GenericArray::from_slice(&hash_nonce.as_bytes()[0..12]); // 96-bit key. - - let plaintext = cipher - .decrypt(nonce, self.mnemonic.as_ref()) - .map_err(|_e| anyhow!("mnemonic unlock invalid."))?; - - String::from_utf8(plaintext).map_err(|_e| anyhow!("mnemonic unlock invalid.")) + pub fn mnemonic(&self, salt: &[u8], lock: &str) -> Result { + self.check_lock(salt, lock)?; + let pbytes = decrypt(salt, lock, &self.mnemonic)?; + String::from_utf8(pbytes).or(Err(anyhow!("mnemonic unlock invalid."))) } - pub fn secret(&self, skey: &[u8], lock: &str) -> Result { - let cipher = Aes256Gcm::new(GenericArray::from_slice(skey)); // 256-bit key. - let hash_nonce = blake3::hash(lock.as_bytes()); - let nonce = GenericArray::from_slice(&hash_nonce.as_bytes()[0..12]); // 96-bit key. - - let plaintext = cipher - .decrypt(nonce, self.secret.as_ref()) - .map_err(|_e| anyhow!("secret unlock invalid."))?; - - Keypair::from_bytes(&plaintext).map_err(|_e| anyhow!("secret unlock invalid.")) + pub fn secret(&self, salt: &[u8], lock: &str) -> Result { + self.check_lock(salt, lock)?; + let pbytes = decrypt(salt, lock, &self.secret)?; + Keypair::from_bytes(&pbytes).or(Err(anyhow!("secret unlock invalid."))) } /// here is zero-copy and unwrap is safe. checked. @@ -204,10 +170,10 @@ impl Account { datetime: v.pop().unwrap().as_i64(), event: EventId::from_hex(v.pop().unwrap().as_str()).unwrap_or(EventId::default()), height: v.pop().unwrap().as_i64() as u64, - avatar: base64::decode(v.pop().unwrap().as_string()).unwrap_or(vec![]), - secret: base64::decode(v.pop().unwrap().as_string()).unwrap_or(vec![]), - mnemonic: base64::decode(v.pop().unwrap().as_string()).unwrap_or(vec![]), - lock: v.pop().unwrap().as_string(), + avatar: base64::decode(v.pop().unwrap().as_str()).unwrap_or(vec![]), + secret: base64::decode(v.pop().unwrap().as_str()).unwrap_or(vec![]), + mnemonic: base64::decode(v.pop().unwrap().as_str()).unwrap_or(vec![]), + lock: base64::decode(v.pop().unwrap().as_string()).unwrap_or(vec![]), name: v.pop().unwrap().as_string(), pass: v.pop().unwrap().as_string(), lang: v.pop().unwrap().as_i64(), @@ -261,7 +227,7 @@ impl Account { self.lang, self.pass, self.name, - self.lock, + base64::encode(&self.lock), base64::encode(&self.mnemonic), base64::encode(&self.secret), base64::encode(&self.avatar), @@ -278,7 +244,7 @@ impl Account { pub fn update(&self, db: &DStorage) -> Result { let sql = format!("UPDATE accounts SET name='{}', lock='{}', mnemonic='{}', secret='{}', avatar='{}', height={}, event='{}', datetime={} WHERE id = {}", self.name, - self.lock, + base64::encode(&self.lock), base64::encode(&self.mnemonic), base64::encode(&self.secret), base64::encode(&self.avatar), diff --git a/src/apps/wallet/models.rs b/src/apps/wallet/models.rs index 98e0543..b88896b 100644 --- a/src/apps/wallet/models.rs +++ b/src/apps/wallet/models.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::ops::AddAssign; use tdn::types::{ primitive::Result, rpc::{json, RpcParam}, @@ -121,7 +120,7 @@ pub(crate) struct Address { /// Encrypted secret key. /// if this address is imported, has this field, /// if this address is generated, no this field. - pub secret: String, + pub secret: Vec, pub balance: String, } @@ -177,13 +176,13 @@ impl Address { index, address, name: format!("Account {}", index), - secret: "".to_owned(), + secret: vec![], balance: "".to_owned(), id: 0, } } - pub fn import(chain: ChainToken, address: String, secret: String) -> Self { + pub fn import(chain: ChainToken, address: String, secret: Vec) -> Self { Self { name: format!("Import {}", &address[2..4]), chain, @@ -210,7 +209,7 @@ impl Address { fn from_values(mut v: Vec) -> Self { Self { balance: v.pop().unwrap().as_string(), - secret: v.pop().unwrap().as_string(), + secret: base64::decode(v.pop().unwrap().as_str()).unwrap_or(vec![]), address: v.pop().unwrap().as_string(), name: v.pop().unwrap().as_string(), index: v.pop().unwrap().as_i64(), @@ -226,7 +225,7 @@ impl Address { self.index, self.name, self.address, - self.secret, + base64::encode(&self.secret), self.balance, ); let id = db.insert(&sql)?; diff --git a/src/apps/wallet/rpc.rs b/src/apps/wallet/rpc.rs index c62747c..d64a2bf 100644 --- a/src/apps/wallet/rpc.rs +++ b/src/apps/wallet/rpc.rs @@ -10,10 +10,12 @@ use tdn_storage::local::DStorage; use tokio::sync::mpsc::Sender; use web3::signing::Key; -use crate::{rpc::RpcState, storage::wallet_db}; +use crate::{rpc::RpcState, storage::wallet_db, utils::crypto::encrypt}; use super::models::{Address, ChainToken, Network, Token}; +const WALLET_DEFAULT_PIN: &'static str = "walletissafe"; + #[inline] fn wallet_list(devices: Vec
) -> RpcParam { let mut results = vec![]; @@ -145,18 +147,17 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { "wallet-import", |gid: GroupId, params: Vec, state: Arc| async move { let chain = ChainToken::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?); - let _lock = params[1].as_str().ok_or(RpcError::ParseError)?; - let secret = params[2].as_str().ok_or(RpcError::ParseError)?; + let secret = params[1].as_str().ok_or(RpcError::ParseError)?; let sk: SecretKey = secret.parse().or(Err(RpcError::ParseError))?; let addr = format!("{:?}", (&sk).address()); let group_lock = state.group.read().await; - let encrypt_secret = "".to_owned(); + let cbytes = encrypt(&group_lock.secret(), WALLET_DEFAULT_PIN, sk.as_ref())?; let db = wallet_db(group_lock.base(), &gid)?; drop(group_lock); - let mut address = Address::import(chain, addr, encrypt_secret); + let mut address = Address::import(chain, addr, cbytes); address.insert(&db)?; Ok(HandleResult::rpc(address.to_rpc())) }, diff --git a/src/group.rs b/src/group.rs index fac3b64..d83bee3 100644 --- a/src/group.rs +++ b/src/group.rs @@ -259,6 +259,10 @@ impl Group { &self.addr } + pub fn secret(&self) -> &[u8] { + &self.secret + } + pub fn base(&self) -> &PathBuf { &self.base } @@ -267,6 +271,14 @@ impl Group { self.sender.clone() } + pub fn check_lock(&self, gid: &GroupId, lock: &str) -> bool { + if let Some(account) = self.accounts.get(gid) { + account.check_lock(&self.secret, lock).is_ok() + } else { + false + } + } + pub fn account(&self, gid: &GroupId) -> Result<&Account> { if let Some(account) = self.accounts.get(gid) { Ok(account) diff --git a/src/rpc.rs b/src/rpc.rs index 70f0c6a..5b8e0a6 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -235,7 +235,7 @@ fn new_rpc_handler( users.push(vec![ gid.to_hex(), user.name.clone(), - user.lock.clone(), + base64::encode(&user.lock), base64::encode(&user.avatar), ]); } @@ -373,6 +373,15 @@ fn new_rpc_handler( }, ); + handler.add_method( + "account-pin-check", + |gid: GroupId, params: Vec, state: Arc| async move { + let lock = params[0].as_str().ok_or(RpcError::ParseError)?; + let res = state.group.read().await.check_lock(&gid, lock); + Ok(HandleResult::rpc(json!([res]))) + }, + ); + handler.add_method( "account-pin", |gid: GroupId, params: Vec, state: Arc| async move { diff --git a/src/utils.rs b/src/utils.rs index 9802e3c..ee2d627 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1 +1,2 @@ +pub(crate) mod crypto; pub(crate) mod device_status; diff --git a/src/utils/crypto.rs b/src/utils/crypto.rs new file mode 100644 index 0000000..bbfbfb3 --- /dev/null +++ b/src/utils/crypto.rs @@ -0,0 +1,107 @@ +use aes_gcm::{ + aead::{generic_array::GenericArray, Aead, NewAead}, + Aes256Gcm, +}; +use sha2::{Digest, Sha256}; + +const FIX_PADDING: [u8; 19] = [ + 69, 83, 83, 69, 70, 111, 114, 68, 97, 116, 97, 83, 101, 99, 117, 114, 105, 116, 121, +]; + +/// Hash the given pin. +pub fn hash_pin(salt: &[u8], pin: &str, index: i64) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(salt); // for avoid same hash when no-pin in other derives. + hasher.update(pin.as_bytes()); + hasher.update(index.to_le_bytes()); // for avoid same hash when no-pin in one device. + hasher.finalize().to_vec() +} + +/// check the pin is the given hash pre-image. +pub fn check_pin(salt: &[u8], pin: &str, index: i64, hash: &[u8]) -> bool { + let mut hasher = Sha256::new(); + hasher.update(salt); + hasher.update(pin.as_bytes()); + hasher.update(index.to_le_bytes()); + let hash_key = hasher.finalize(); + &hash_key[..] == hash +} + +fn build_cipher(salt: &[u8], pin: &str) -> Aes256Gcm { + let mut hasher = blake3::Hasher::new(); + hasher.update(salt); + hasher.update(pin.as_bytes()); + hasher.update(&FIX_PADDING); + let hash_key = hasher.finalize(); + Aes256Gcm::new(GenericArray::from_slice(hash_key.as_bytes())) // 256-bit key. +} + +/// encrypted bytes. +pub fn encrypt(salt: &[u8], pin: &str, ptext: &[u8]) -> anyhow::Result> { + let cipher = build_cipher(salt, pin); + + let mut nonce = Sha256::new(); + nonce.update(pin.as_bytes()); + nonce.update(&FIX_PADDING); + let res = nonce.finalize(); + let nonce = GenericArray::from_slice(&res[0..12]); // 96-bit key. + + cipher + .encrypt(nonce, ptext) + .or(Err(anyhow!("encrypt data failure."))) +} + +pub fn encrypt_multiple(salt: &[u8], pin: &str, ptext: Vec<&[u8]>) -> anyhow::Result>> { + let cipher = build_cipher(salt, pin); + + let mut nonce = Sha256::new(); + nonce.update(pin.as_bytes()); + nonce.update(&FIX_PADDING); + let res = nonce.finalize(); + let nonce = GenericArray::from_slice(&res[0..12]); // 96-bit key. + + let mut ebytes = vec![]; + for p in ptext { + ebytes.push( + cipher + .encrypt(nonce, p) + .or(Err(anyhow!("encrypt data failure.")))?, + ); + } + Ok(ebytes) +} + +/// decrypted bytes. +pub fn decrypt(salt: &[u8], pin: &str, ctext: &[u8]) -> anyhow::Result> { + let cipher = build_cipher(salt, pin); + + let mut nonce = Sha256::new(); + nonce.update(pin.as_bytes()); + nonce.update(&FIX_PADDING); + let res = nonce.finalize(); + let nonce = GenericArray::from_slice(&res[0..12]); // 96-bit key. + + cipher + .decrypt(nonce, ctext) + .or(Err(anyhow!("decrypt data failure."))) +} + +pub fn decrypt_multiple(salt: &[u8], pin: &str, ctext: Vec<&[u8]>) -> anyhow::Result>> { + let cipher = build_cipher(salt, pin); + + let mut nonce = Sha256::new(); + nonce.update(pin.as_bytes()); + nonce.update(&FIX_PADDING); + let res = nonce.finalize(); + let nonce = GenericArray::from_slice(&res[0..12]); // 96-bit key. + + let mut pbytes = vec![]; + for c in ctext { + pbytes.push( + cipher + .decrypt(nonce, c) + .or(Err(anyhow!("decrypt data failure.")))?, + ); + } + Ok(pbytes) +}