diff --git a/token-core/tcx-keystore/src/identity.rs b/token-core/tcx-keystore/src/identity.rs index 91438224..f87e3f29 100644 --- a/token-core/tcx-keystore/src/identity.rs +++ b/token-core/tcx-keystore/src/identity.rs @@ -113,107 +113,116 @@ impl Identity { } pub fn encrypt_ipfs(&self, plaintext: &str) -> Result { - let iv: [u8; 16] = random_u8_16(); - self.encrypt_ipfs_wth_timestamp_iv(plaintext, unix_timestamp(), &iv) + encrypt_ipfs_with_enc_key(&self.enc_key, plaintext) } - fn encrypt_ipfs_wth_timestamp_iv( + + pub fn decrypt_ipfs(&self, ciphertext: &str) -> Result { + decrypt_ipfs_with_enc_key(ciphertext, &self.ipfs_id, &self.enc_key) + } + + pub fn sign_authentication_message( &self, - plaintext: &str, - timestamp: u64, - iv: &[u8; 16], + access_time: u64, + device_token: &str, + unlocker: &Unlocker, ) -> Result { - let mut header = Vec::new(); - - header.write_u8(0x03)?; - header.write_all(×tamp.to_le_bytes()[..4])?; - header.write_all(iv)?; + let enc_auth_key = unlocker.decrypt_enc_pair(&self.enc_auth_key)?; + let mut signature = Secp256k1PrivateKey::from_slice(&enc_auth_key)?.sign_recoverable( + &keccak256(format!("{}.{}.{}", access_time, self.identifier, device_token).as_bytes()), + )?; + signature[64] += 27; + Ok(signature.to_0x_hex()) + } +} - let enc_key = Vec::from_hex(&self.enc_key)?; +pub fn decrypt_ipfs_with_enc_key(ciphertext: &str, ipfs_id: &str, enc_key: &str) -> Result { + let ciphertext = Vec::::from_hex(ciphertext)?; + if ciphertext.len() <= 21 { + return Err(Error::InvalidEncryptionData.into()); + } - let ciphertext = encrypt_pkcs7(plaintext.as_bytes(), &enc_key[0..16], iv)?; - let hash = keccak256(&[header.clone(), merkle_hash(&ciphertext).to_vec()].concat()); - let mut signature = Secp256k1PrivateKey::from_slice(&enc_key)?.sign_recoverable(&hash)?; - //ETH-compatible ec_recover, in chain_id = 1 case, v = 27 + rec_id - signature[64] += 27; + let mut rdr = Cursor::new(&ciphertext); + let mut header = vec![]; - let var_len = VarInt(ciphertext.len() as u64); + let version = rdr.read_u8()?; + if version != 0x03 { + return Err(Error::UnsupportEncryptionDataVersion.into()); + } + header.write_u8(version)?; + header.write_u32::(rdr.read_u32::()?)?; - let mut payload = vec![]; - payload.write_all(&header)?; - var_len.consensus_encode(&mut payload)?; - payload.write_all(&ciphertext)?; - payload.write_all(&signature)?; + let mut iv = [0u8; 16]; + rdr.read_exact(&mut iv)?; + header.write_all(&iv)?; - Ok(payload.to_hex()) + let var_len = VarInt::consensus_decode(&mut rdr)?; + if var_len.0 as usize != ciphertext.len() - 21 - 65 - var_len.len() { + return Err(Error::InvalidEncryptionData.into()); } - pub fn decrypt_ipfs(&self, ciphertext: &str) -> Result { - let ciphertext = Vec::::from_hex(ciphertext)?; - if ciphertext.len() <= 21 { - return Err(Error::InvalidEncryptionData.into()); - } + let mut enc_data = vec![0u8; var_len.0 as usize]; + rdr.read_exact(&mut enc_data)?; - let mut rdr = Cursor::new(&ciphertext); - let mut header = vec![]; + let mut signature = [0u8; 64]; + rdr.read_exact(&mut signature)?; - let version = rdr.read_u8()?; - if version != 0x03 { - return Err(Error::UnsupportEncryptionDataVersion.into()); - } - header.write_u8(version)?; - header.write_u32::(rdr.read_u32::()?)?; + let recover_id = RecoveryId::from_i32(rdr.read_u8()? as i32 - 27)?; - let mut iv = [0u8; 16]; - rdr.read_exact(&mut iv)?; - header.write_all(&iv)?; + let hash = keccak256( + [header, merkle_hash(&enc_data).to_vec()] + .concat() + .as_slice(), + ); - let var_len = VarInt::consensus_decode(&mut rdr)?; - if var_len.0 as usize != ciphertext.len() - 21 - 65 - var_len.len() { - return Err(Error::InvalidEncryptionData.into()); - } + let message = Message::from_slice(&hash)?; + let sig = RecoverableSignature::from_compact(&signature, recover_id)?; + let pub_key = Secp256k1::new().recover_ecdsa(&message, &sig)?; + let calculated_ipfs_id = Identity::calculate_ipfs_id(&pub_key); - let mut enc_data = vec![0u8; var_len.0 as usize]; - rdr.read_exact(&mut enc_data)?; + if ipfs_id != calculated_ipfs_id { + return Err(Error::InvalidEncryptionDataSignature.into()); + } - let mut signature = [0u8; 64]; - rdr.read_exact(&mut signature)?; + let enc_key = Vec::from_hex(&enc_key)?; + let plaintext = decrypt_pkcs7(&enc_data, &enc_key[..16], &iv)?; - let recover_id = RecoveryId::from_i32(rdr.read_u8()? as i32 - 27)?; + Ok(String::from_utf8(plaintext)?) +} - let hash = keccak256( - [header, merkle_hash(&enc_data).to_vec()] - .concat() - .as_slice(), - ); +pub fn encrypt_ipfs_with_enc_key(enc_key: &str, plaintext: &str) -> Result { + let iv: [u8; 16] = random_u8_16(); + encrypt_ipfs_wth_timestamp_iv(&enc_key, plaintext, unix_timestamp(), &iv) +} - let message = Message::from_slice(&hash)?; - let sig = RecoverableSignature::from_compact(&signature, recover_id)?; - let pub_key = Secp256k1::new().recover_ecdsa(&message, &sig)?; - let ipfs_id = Self::calculate_ipfs_id(&pub_key); +fn encrypt_ipfs_wth_timestamp_iv( + enc_key: &str, + plaintext: &str, + timestamp: u64, + iv: &[u8; 16], +) -> Result { + let mut header = Vec::new(); - if self.ipfs_id != ipfs_id { - return Err(Error::InvalidEncryptionDataSignature.into()); - } + header.write_u8(0x03)?; + header.write_all(×tamp.to_le_bytes()[..4])?; + header.write_all(iv)?; - let enc_key = Vec::from_hex(&self.enc_key)?; - let plaintext = decrypt_pkcs7(&enc_data, &enc_key[..16], &iv)?; + let enc_key = Vec::from_hex_auto(&enc_key)?; - Ok(String::from_utf8(plaintext)?) - } + let ciphertext = encrypt_pkcs7(plaintext.as_bytes(), &enc_key[0..16], iv)?; + let hash = keccak256(&[header.clone(), merkle_hash(&ciphertext).to_vec()].concat()); + let mut signature = Secp256k1PrivateKey::from_slice(&enc_key)?.sign_recoverable(&hash)?; + //ETH-compatible ec_recover, in chain_id = 1 case, v = 27 + rec_id + signature[64] += 27; - pub fn sign_authentication_message( - &self, - access_time: u64, - device_token: &str, - unlocker: &Unlocker, - ) -> Result { - let enc_auth_key = unlocker.decrypt_enc_pair(&self.enc_auth_key)?; - let mut signature = Secp256k1PrivateKey::from_slice(&enc_auth_key)?.sign_recoverable( - &keccak256(format!("{}.{}.{}", access_time, self.identifier, device_token).as_bytes()), - )?; - signature[64] += 27; - Ok(signature.to_0x_hex()) - } + let var_len = VarInt(ciphertext.len() as u64); + + let mut payload = vec![]; + payload.write_all(&header)?; + var_len.consensus_encode(&mut payload)?; + payload.write_all(&ciphertext)?; + payload.write_all(&signature)?; + + Ok(payload.to_hex()) } #[cfg(test)] @@ -224,6 +233,7 @@ mod test { use tcx_constants::CurveType; use tcx_crypto::Key; + use crate::identity::encrypt_ipfs_wth_timestamp_iv; use crate::{keystore::IdentityNetwork, Error, HdKeystore, Metadata, PrivateKeystore}; #[test] @@ -313,9 +323,7 @@ mod test { for t in test_cases { let iv: [u8; 16] = Vec::from_hex(t.1).unwrap().try_into().unwrap(); assert_eq!( - identity - .encrypt_ipfs_wth_timestamp_iv(t.0, unix_timestamp, &iv) - .unwrap(), + encrypt_ipfs_wth_timestamp_iv(&identity.enc_key, t.0, unix_timestamp, &iv).unwrap(), t.2 ); assert_eq!(identity.decrypt_ipfs(t.2).unwrap(), t.0); diff --git a/token-core/tcx-migration/src/legacy_ipfs.rs b/token-core/tcx-migration/src/legacy_ipfs.rs new file mode 100644 index 00000000..467b43d7 --- /dev/null +++ b/token-core/tcx-migration/src/legacy_ipfs.rs @@ -0,0 +1,51 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::io::Read; +use tcx_constants::{coin_info_from_param, CurveType}; +use tcx_crypto::{Crypto, EncPair, Key}; +use tcx_keystore::identity::Identity; +use tcx_keystore::keystore::{IdentityNetwork, Keystore, Metadata, Store}; +use tcx_keystore::{ + fingerprint_from_private_key, fingerprint_from_seed, mnemonic_to_seed, Address, HdKeystore, + PrivateKeystore, Result, Source, +}; +use tcx_primitive::{PrivateKey, Secp256k1PrivateKey, TypedPublicKey}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IpfsInfo { + pub identifier: String, + pub ipfs_id: String, + pub enc_key: String, +} + +pub fn read_legacy_ipfs_info(filepath: &str) -> Result { + let mut identify_file = fs::File::open(&filepath)?; + + let mut json_str = String::new(); + identify_file.read_to_string(&mut json_str)?; + let ipfs_info: IpfsInfo = serde_json::from_str(&json_str)?; + Ok(ipfs_info) +} + +#[cfg(test)] +mod tests { + use super::read_legacy_ipfs_info; + #[test] + fn test_scan_tcx_legacy_keystores() { + let ipfs_info = read_legacy_ipfs_info("../test-data/wallets/identity.json").unwrap(); + assert_eq!( + ipfs_info.identifier, + "im18MDKM8hcTykvMmhLnov9m2BaFqsdjoA7cwNg" + ); + assert_eq!( + ipfs_info.ipfs_id, + "QmSTTidyfa4np9ak9BZP38atuzkCHy4K59oif23f4dNAGU" + ); + assert_eq!( + ipfs_info.enc_key, + "9513617c9b398edebfb46080a8f0cf6cab6763866bb06daa63503722bea78907" + ); + } +} diff --git a/token-core/tcx-migration/src/lib.rs b/token-core/tcx-migration/src/lib.rs index b6bcbe71..445a231c 100644 --- a/token-core/tcx-migration/src/lib.rs +++ b/token-core/tcx-migration/src/lib.rs @@ -1,2 +1,3 @@ pub mod keystore_upgrade; +pub mod legacy_ipfs; pub mod migration; diff --git a/token-core/tcx/src/handler.rs b/token-core/tcx/src/handler.rs index 9bafa6bc..18f1e99c 100644 --- a/token-core/tcx/src/handler.rs +++ b/token-core/tcx/src/handler.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use tcx_eos::encode_eos_wif; use tcx_eth2::transaction::{SignBlsToExecutionChangeParam, SignBlsToExecutionChangeResult}; use tcx_keystore::keystore::IdentityNetwork; +use tcx_migration::legacy_ipfs; use tcx_common::{FromHex, ToHex}; use tcx_primitive::{ @@ -17,12 +18,12 @@ use tcx_primitive::{ use tcx_btc_kin::WIFDisplay; use tcx_keystore::{ - fingerprint_from_mnemonic, fingerprint_from_private_key, Keystore, KeystoreGuard, + fingerprint_from_mnemonic, fingerprint_from_private_key, identity, Keystore, KeystoreGuard, SignatureParameters, Signer, }; use tcx_keystore::{Account, HdKeystore, Metadata, PrivateKeystore, Source}; -use anyhow::anyhow; +use anyhow::{anyhow, ensure}; use tcx_crypto::{XPUB_COMMON_IV, XPUB_COMMON_KEY_128}; use tcx_filecoin::KeyInfo; @@ -1098,12 +1099,21 @@ pub(crate) fn encrypt_data_to_ipfs(data: &[u8]) -> Result> { let param = EncryptDataToIpfsParam::decode(data).expect("EncryptDataToIpfsParam"); let map = KEYSTORE_MAP.read(); - let Some(identity_ks) = map.values().find(|ks| ks.identity().identifier == param.identifier) else { - return Err(anyhow::anyhow!("identity_not_found")); + let cipher_text = if let Some(identity_ks) = map + .values() + .find(|ks| ks.identity().identifier == param.identifier) + { + identity_ks.identity().encrypt_ipfs(¶m.content)? + } else { + let legacy_identify_path = &format!("{}/identity.json", LEGACY_WALLET_FILE_DIR.read()); + let legacy_ipfs_info = legacy_ipfs::read_legacy_ipfs_info(&legacy_identify_path)?; + ensure!( + legacy_ipfs_info.identifier == param.identifier, + "wallet_not_found" + ); + identity::encrypt_ipfs_with_enc_key(&legacy_ipfs_info.enc_key, ¶m.content)? }; - let cipher_text = identity_ks.identity().encrypt_ipfs(¶m.content)?; - let output = EncryptDataToIpfsResult { identifier: param.identifier.to_string(), encrypted: cipher_text, @@ -1116,12 +1126,21 @@ pub(crate) fn decrypt_data_from_ipfs(data: &[u8]) -> Result> { let param = DecryptDataFromIpfsParam::decode(data).expect("DecryptDataFromIpfsParam"); let map = KEYSTORE_MAP.read(); - let Some(identity_ks) = map.values().find(|ks| ks.identity().identifier == param.identifier) else { - return Err(anyhow::anyhow!("identity_not_found")); + let content = if let Some(identity_ks) = map + .values() + .find(|ks| ks.identity().identifier == param.identifier) + { + identity_ks.identity().decrypt_ipfs(¶m.encrypted)? + } else { + let legacy_identify_path = &format!("{}/identity.json", LEGACY_WALLET_FILE_DIR.read()); + let legacy_ipfs_data = legacy_ipfs::read_legacy_ipfs_info(&legacy_identify_path)?; + identity::decrypt_ipfs_with_enc_key( + ¶m.encrypted, + &legacy_ipfs_data.ipfs_id, + &legacy_ipfs_data.enc_key, + )? }; - let content = identity_ks.identity().decrypt_ipfs(¶m.encrypted)?; - let output = DecryptDataFromIpfsResult { identifier: param.identifier.to_string(), content, diff --git a/token-core/tcx/tests/other_test.rs b/token-core/tcx/tests/other_test.rs index 04a9f9ff..4c1740f5 100644 --- a/token-core/tcx/tests/other_test.rs +++ b/token-core/tcx/tests/other_test.rs @@ -1,3 +1,5 @@ +use std::fs; + use common::run_test; use serial_test::serial; @@ -243,6 +245,35 @@ pub fn test_ipfs_encrypt_and_decrypt() { }) } +#[test] +#[serial] +pub fn test_ipfs_encrypt_and_decrypt_before_migrate() { + run_test(|| { + fs::copy( + "../test-data/wallets/identity.json", + "/tmp/imtoken/wallets/identity.json", + ) + .unwrap(); + let content = "imToken".to_string(); + let param = EncryptDataToIpfsParam { + identifier: "im18MDKM8hcTykvMmhLnov9m2BaFqsdjoA7cwNg".to_string(), + content: content.clone(), + }; + let ret = call_api("encrypt_data_to_ipfs", param).unwrap(); + let resp: EncryptDataToIpfsResult = + EncryptDataToIpfsResult::decode(ret.as_slice()).unwrap(); + assert!(!resp.encrypted.is_empty()); + let param = DecryptDataFromIpfsParam { + identifier: "im18MDKM8hcTykvMmhLnov9m2BaFqsdjoA7cwNg".to_string(), + encrypted: resp.encrypted, + }; + let ret = call_api("decrypt_data_from_ipfs", param).unwrap(); + let resp: DecryptDataFromIpfsResult = + DecryptDataFromIpfsResult::decode(ret.as_slice()).unwrap(); + assert_eq!(content, resp.content); + }) +} + #[test] #[serial] pub fn test_sign_authentication_message() {