diff --git a/.vscode/settings.json b/.vscode/settings.json index d4c0ca5..3a6d502 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true - } -} + }, + "cSpell.words": [ + "ecdh" + ] +} \ No newline at end of file diff --git a/chain-common/proto/api.proto b/chain-common/proto/api.proto index 0b74a67..f037378 100644 --- a/chain-common/proto/api.proto +++ b/chain-common/proto/api.proto @@ -9,6 +9,7 @@ import "stored-key.proto"; import "transaction.proto"; import "validation.proto"; import "persona.proto"; +import "post-encryption.proto"; message MWRequest { oneof request { @@ -36,6 +37,8 @@ message MWRequest { GenerateMnemonicParam param_generate_mnemonic = 25; PersonaGenerationParam param_generate_persona = 26; + + PostEncryptionParam param_post_encryption = 27; } } @@ -64,6 +67,8 @@ message MWResponse { GenerateMnemonicResp resp_generate_mnemonic = 24; PersonaGenerationResp resp_generate_persona = 25; + + PostEncryptedResp resp_post_encryption = 26; } } diff --git a/chain-common/proto/base.proto b/chain-common/proto/base.proto index cca013c..02d5608 100644 --- a/chain-common/proto/base.proto +++ b/chain-common/proto/base.proto @@ -40,10 +40,39 @@ message StoredKeyAccountInfo { string extendedPublicKey = 5; } +enum Curve { + Secp256k1 = 0; + Ed25519 = 1; +} + message EncryptOption { enum Version { V37 = 0; V38 = 1; } Version version = 1; +} + +message JWK { + string crv = 1; + bool ext = 3; + string x = 4; + string y = 5; + repeated string key_ops = 6; + string kty = 7; + optional string d = 8; +} + +message AesJWK { + string alg = 1; + bool ext = 2; + string k = 3; + repeated string key_ops = 4; + string kty = 5; +} + +message E2EEncryptParam { + bytes localKeyData = 1; + map target = 2; + bytes authorPrivateKey = 3; } \ No newline at end of file diff --git a/chain-common/proto/persona.proto b/chain-common/proto/persona.proto index 32880ee..3edc8ac 100644 --- a/chain-common/proto/persona.proto +++ b/chain-common/proto/persona.proto @@ -9,36 +9,15 @@ message PersonaGenerationParam { string mnemonic = 1; string password = 2; string path = 3; - enum Curve { - Secp256k1 = 0; - Ed25519 = 1; - } Curve curve = 4; EncryptOption option = 5; } message PersonaGenerationResp { string identifier = 1; - JWKResp privateKey = 2; - JWKResp publicKey = 3; - optional AesJWKResp localKey = 4; + JWK privateKey = 2; + JWK publicKey = 3; + optional AesJWK localKey = 4; EncryptOption option = 5; } -message JWKResp { - string crv = 1; - bool ext = 3; - string x = 4; - string y = 5; - repeated string key_ops = 6; - string kty = 7; - optional string d = 8; -} - -message AesJWKResp { - string alg = 1; - bool ext = 2; - string k = 3; - repeated string key_ops = 4; - string kty = 5; -} \ No newline at end of file diff --git a/chain-common/proto/post-encryption.proto b/chain-common/proto/post-encryption.proto new file mode 100644 index 0000000..44eee5a --- /dev/null +++ b/chain-common/proto/post-encryption.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package api; + +import "base.proto"; + +enum PublicKeyAlgorithm { + Ed25519Algr = 0; + Secp256p1Algr = 1; + Secp256k1Algr = 2; +} + +message PostEncryptionParam { + EncryptOption.Version version = 1; + bool isPlublic = 2; + string content = 3; + string network = 4; + optional bytes authorPublicKeyData = 5; + optional string authorUserId = 6; + optional PublicKeyAlgorithm authorPublicKeyAlgr = 7; + optional E2EEncryptParam param = 8; +} + +message E2EEncryptionResult { + optional bytes iv = 1; + bytes encryptedPostKeyData = 2; + optional bytes ephemeralPublicKeyData = 3; +} + +message PostEncryptedResp { + string content = 1; + string postIdentifier = 2; + bytes postKey = 3; + map results = 4; +} \ No newline at end of file diff --git a/chain-common/src/convert.rs b/chain-common/src/convert.rs index 5c8b99a..1064fba 100644 --- a/chain-common/src/convert.rs +++ b/chain-common/src/convert.rs @@ -2,8 +2,7 @@ use std::convert::{From, TryFrom}; use std::str::FromStr; use crate::generated::api::{ - encrypt_option::Version, mw_response::Response, persona_generation_param::Curve, MwResponse, - MwResponseError, + encrypt_option::Version, mw_response::Response, Curve, MwResponse, MwResponseError, }; use crypto::Error as CryptoError; @@ -16,6 +15,13 @@ impl From for MwResponseError { } } +impl From for MwResponse { + fn from(err: CryptoError) -> Self { + let resp_error: MwResponseError = err.into(); + resp_error.into() + } +} + impl From for MwResponseError { fn from(err: crypto::jwk::BIP32Error) -> Self { Self { @@ -46,6 +52,14 @@ impl From> for MwResponse { } } +impl From for MwResponse { + fn from(response: Response) -> Self { + Self { + response: Some(response), + } + } +} + impl FromStr for Curve { type Err = MwResponseError; fn from_str(s: &str) -> Result { diff --git a/chain-common/src/generated/api.rs b/chain-common/src/generated/api.rs index 509ca33..cea48ce 100644 --- a/chain-common/src/generated/api.rs +++ b/chain-common/src/generated/api.rs @@ -48,6 +48,45 @@ pub mod encrypt_option { V38 = 1, } } +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Jwk { + #[prost(string, tag="1")] + pub crv: ::prost::alloc::string::String, + #[prost(bool, tag="3")] + pub ext: bool, + #[prost(string, tag="4")] + pub x: ::prost::alloc::string::String, + #[prost(string, tag="5")] + pub y: ::prost::alloc::string::String, + #[prost(string, repeated, tag="6")] + pub key_ops: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, tag="7")] + pub kty: ::prost::alloc::string::String, + #[prost(string, optional, tag="8")] + pub d: ::core::option::Option<::prost::alloc::string::String>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AesJwk { + #[prost(string, tag="1")] + pub alg: ::prost::alloc::string::String, + #[prost(bool, tag="2")] + pub ext: bool, + #[prost(string, tag="3")] + pub k: ::prost::alloc::string::String, + #[prost(string, repeated, tag="4")] + pub key_ops: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(string, tag="5")] + pub kty: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct E2eEncryptParam { + #[prost(bytes="vec", tag="1")] + pub local_key_data: ::prost::alloc::vec::Vec, + #[prost(map="string, bytes", tag="2")] + pub target: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::vec::Vec>, + #[prost(bytes="vec", tag="3")] + pub author_private_key: ::prost::alloc::vec::Vec, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum Coin { @@ -75,6 +114,12 @@ pub enum StoredKeyExportType { MnemonicExportType = 1, KeyStoreJsonExportType = 2, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum Curve { + Secp256k1 = 0, + Ed25519 = 1, +} /// Create a new account to the StoredKey at specific derivation path. Fail if the StoredKey is not a Hd StoredKey #[derive(Clone, PartialEq, ::prost::Message)] pub struct CreateStoredKeyNewAccountAtPathParam { @@ -378,66 +423,73 @@ pub struct PersonaGenerationParam { pub password: ::prost::alloc::string::String, #[prost(string, tag="3")] pub path: ::prost::alloc::string::String, - #[prost(enumeration="persona_generation_param::Curve", tag="4")] + #[prost(enumeration="Curve", tag="4")] pub curve: i32, #[prost(message, optional, tag="5")] pub option: ::core::option::Option, } -/// Nested message and enum types in `PersonaGenerationParam`. -pub mod persona_generation_param { - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] - #[repr(i32)] - pub enum Curve { - Secp256k1 = 0, - Ed25519 = 1, - } -} #[derive(Clone, PartialEq, ::prost::Message)] pub struct PersonaGenerationResp { #[prost(string, tag="1")] pub identifier: ::prost::alloc::string::String, #[prost(message, optional, tag="2")] - pub private_key: ::core::option::Option, + pub private_key: ::core::option::Option, #[prost(message, optional, tag="3")] - pub public_key: ::core::option::Option, + pub public_key: ::core::option::Option, #[prost(message, optional, tag="4")] - pub local_key: ::core::option::Option, + pub local_key: ::core::option::Option, #[prost(message, optional, tag="5")] pub option: ::core::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] -pub struct JwkResp { - #[prost(string, tag="1")] - pub crv: ::prost::alloc::string::String, - #[prost(bool, tag="3")] - pub ext: bool, - #[prost(string, tag="4")] - pub x: ::prost::alloc::string::String, - #[prost(string, tag="5")] - pub y: ::prost::alloc::string::String, - #[prost(string, repeated, tag="6")] - pub key_ops: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, tag="7")] - pub kty: ::prost::alloc::string::String, - #[prost(string, optional, tag="8")] - pub d: ::core::option::Option<::prost::alloc::string::String>, -} -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AesJwkResp { - #[prost(string, tag="1")] - pub alg: ::prost::alloc::string::String, +pub struct PostEncryptionParam { + #[prost(enumeration="encrypt_option::Version", tag="1")] + pub version: i32, #[prost(bool, tag="2")] - pub ext: bool, + pub is_plublic: bool, #[prost(string, tag="3")] - pub k: ::prost::alloc::string::String, - #[prost(string, repeated, tag="4")] - pub key_ops: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, tag="5")] - pub kty: ::prost::alloc::string::String, + pub content: ::prost::alloc::string::String, + #[prost(string, tag="4")] + pub network: ::prost::alloc::string::String, + #[prost(bytes="vec", optional, tag="5")] + pub author_public_key_data: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(string, optional, tag="6")] + pub author_user_id: ::core::option::Option<::prost::alloc::string::String>, + #[prost(enumeration="PublicKeyAlgorithm", optional, tag="7")] + pub author_public_key_algr: ::core::option::Option, + #[prost(message, optional, tag="8")] + pub param: ::core::option::Option, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct E2eEncryptionResult { + #[prost(bytes="vec", optional, tag="1")] + pub iv: ::core::option::Option<::prost::alloc::vec::Vec>, + #[prost(bytes="vec", tag="2")] + pub encrypted_post_key_data: ::prost::alloc::vec::Vec, + #[prost(bytes="vec", optional, tag="3")] + pub ephemeral_public_key_data: ::core::option::Option<::prost::alloc::vec::Vec>, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PostEncryptedResp { + #[prost(string, tag="1")] + pub content: ::prost::alloc::string::String, + #[prost(string, tag="2")] + pub post_identifier: ::prost::alloc::string::String, + #[prost(bytes="vec", tag="3")] + pub post_key: ::prost::alloc::vec::Vec, + #[prost(map="string, message", tag="4")] + pub results: ::std::collections::HashMap<::prost::alloc::string::String, E2eEncryptionResult>, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum PublicKeyAlgorithm { + Ed25519Algr = 0, + Secp256p1Algr = 1, + Secp256k1Algr = 2, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct MwRequest { - #[prost(oneof="mw_request::Request", tags="1, 2, 3, 4, 5, 10, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 24, 25, 26")] + #[prost(oneof="mw_request::Request", tags="1, 2, 3, 4, 5, 10, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 24, 25, 26, 27")] pub request: ::core::option::Option, } /// Nested message and enum types in `MWRequest`. @@ -482,11 +534,13 @@ pub mod mw_request { ParamGenerateMnemonic(super::GenerateMnemonicParam), #[prost(message, tag="26")] ParamGeneratePersona(super::PersonaGenerationParam), + #[prost(message, tag="27")] + ParamPostEncryption(super::PostEncryptionParam), } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct MwResponse { - #[prost(oneof="mw_response::Response", tags="1, 2, 3, 4, 5, 6, 11, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25")] + #[prost(oneof="mw_response::Response", tags="1, 2, 3, 4, 5, 6, 11, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26")] pub response: ::core::option::Option, } /// Nested message and enum types in `MWResponse`. @@ -529,6 +583,8 @@ pub mod mw_response { RespGenerateMnemonic(super::GenerateMnemonicResp), #[prost(message, tag="25")] RespGeneratePersona(super::PersonaGenerationResp), + #[prost(message, tag="26")] + RespPostEncryption(super::PostEncryptedResp), } } #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 62c5bde..cac1079 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -23,4 +23,7 @@ serde_json = "1.0" base64 = "0.13.0" pbkdf2 = { version = "0.11", default-features = false } hmac = { version = "0.12.1" } -ctr = { version = "0.9.1" } \ No newline at end of file +ctr = { version = "0.9.1" } +aes-gcm = { version = "0.9.4" } +rmp = { version = "0.8.1" } + diff --git a/crypto/src/aes_gcm.rs b/crypto/src/aes_gcm.rs new file mode 100644 index 0000000..0abd817 --- /dev/null +++ b/crypto/src/aes_gcm.rs @@ -0,0 +1,26 @@ +use aes_gcm::aead::{Aead, NewAead}; +use aes_gcm::aes::{cipher::consts::U16, Aes256}; +use aes_gcm::{AesGcm, Key, Nonce}; + +use super::Error; + +type Aes256GCM = AesGcm; + +pub fn aes_encrypt(iv: &[u8], key: &[u8], content: &[u8]) -> Result, Error> { + let key = Key::from_slice(key); + let nonce = Nonce::from_slice(iv); + let cipher = Aes256GCM::new(key); + cipher + .encrypt(nonce, content) + .map_err(|_| Error::InvalidCiphertext) +} + +pub fn aes_decrypt(iv: &[u8], key: &[u8], encrypted_content: &[u8]) -> Result, Error> { + let nonce = Nonce::from_slice(iv); + let key = Key::from_slice(key); + let cipher = Aes256GCM::new(key); + + cipher + .decrypt(nonce, encrypted_content.as_ref()) + .map_err(|_| Error::InvalidCiphertext) +} diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 9c8fcbb..2bbb5d5 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -1,4 +1,5 @@ pub mod aes; +pub mod aes_gcm; pub mod aes_params; pub mod curve; pub mod hash; @@ -11,6 +12,9 @@ pub mod bip32; pub mod bip39; pub mod number_util; +pub mod payload_encode_v37; +pub mod payload_encode_v38; +pub mod post_encryption; pub mod jwk; pub mod pbkdf2; @@ -42,6 +46,8 @@ pub enum Error { NotSupportedCurve, NotSupportedCipher, + + InvalidLocalKey, } impl Error { @@ -60,6 +66,7 @@ impl Error { Error::NotSupportedPublicKeyType => "-3011".to_owned(), Error::NotSupportedCurve => "-3012".to_owned(), Error::NotSupportedCipher => "-3013".to_owned(), + Error::InvalidLocalKey => "-3014".to_owned(), } } @@ -78,6 +85,9 @@ impl Error { Error::NotSupportedPublicKeyType => "Not supported public key type".to_owned(), Error::NotSupportedCurve => "Not supported curve".to_owned(), Error::NotSupportedCipher => "Not supported cipher type".to_owned(), + Error::InvalidLocalKey => { + "Invalid local key. Local key is required to encrypt private message".to_owned() + } } } } diff --git a/crypto/src/payload_encode_v37.rs b/crypto/src/payload_encode_v37.rs new file mode 100644 index 0000000..53579de --- /dev/null +++ b/crypto/src/payload_encode_v37.rs @@ -0,0 +1,111 @@ +use rmp::encode::*; + +use super::Error; + +enum Index { + Version = 0, + AuthorNetwork = 1, + AuthorID = 2, + AuthorPublicKeyAlgorithm = 3, + AuthorPublicKey = 4, + Encryption = 5, + Data = 6, +} + +pub fn encode_with_container( + network: &str, + author_id: &str, + algr: u8, + author_pub_key: &[u8], + aes_key: &[u8], + iv: &[u8], + encrypted: &[u8], +) -> Result, Error> { + let encoded_without_container = encode_v37( + network, + author_id, + algr, + author_pub_key, + aes_key, + iv, + encrypted, + ) + .map_err(|_| Error::InvalidCiphertext)?; + let mut buf = Vec::new(); + write_map_len(&mut buf, 2).map_err(|_| Error::InvalidCiphertext)?; + write_sint(&mut buf, 0).map_err(|_| Error::InvalidCiphertext)?; + write_bin(&mut buf, &encoded_without_container).map_err(|_| Error::InvalidCiphertext)?; + Ok(buf) +} + +fn encode_v37( + network: &str, + author_id: &str, + algr: u8, + author_pub_key: &[u8], + aes_key: &[u8], + iv: &[u8], + encrypted: &[u8], +) -> Result, Error> { + let mut buf = Vec::new(); + // pack length + write_map_len(&mut buf, 6).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::AuthorNetwork as i64).map_err(|_| Error::InvalidCiphertext)?; + write_str(&mut buf, network).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::AuthorID as i64).map_err(|_| Error::InvalidCiphertext)?; + write_str(&mut buf, author_id).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::AuthorPublicKeyAlgorithm as i64) + .map_err(|_| Error::InvalidCiphertext)?; + write_sint(&mut buf, algr as i64).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::AuthorPublicKey as i64).map_err(|_| Error::InvalidCiphertext)?; + write_bin(&mut buf, author_pub_key).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::Encryption as i64).map_err(|_| Error::InvalidCiphertext)?; + write_array_len(&mut buf, 3).map_err(|_| Error::InvalidCiphertext)?; + write_sint(&mut buf, 0).map_err(|_| Error::InvalidCiphertext)?; + write_bin(&mut buf, aes_key).map_err(|_| Error::InvalidCiphertext)?; + write_bin(&mut buf, iv).map_err(|_| Error::InvalidCiphertext)?; + + write_sint(&mut buf, Index::Data as i64).map_err(|_| Error::InvalidCiphertext)?; + write_bin(&mut buf, encrypted).map_err(|_| Error::InvalidCiphertext)?; + + Ok(buf.to_vec()) +} + +#[cfg(test)] +mod tests { + // use super::*; + // use rmp::encode::*; + + // const IV_SIZE: usize = 16; + // const AES_KEY_SIZE: usize = 32; + + #[test] + fn test_encode_v37() { + // let post_iv = random_iv(IV_SIZE); + // let post_key_iv = random_iv(AES_KEY_SIZE); + // let author_key = random_iv(33); + // let content = "sample text"; + + // let encrypted_message = aes_encrypt(&post_iv, &post_key_iv, &content.as_bytes()).unwrap(); + // let message = "hello world"; + // let network = "localhost"; + // let authorId = "alice"; + // let algr = 2; + // let encode_with_no_sign = encode_with_container( + // &network, + // &authorId, + // algr, + // &author_key, + // &post_key_iv, + // &post_iv, + // &encrypted_message, + // ) + // .unwrap(); + // assert_eq!(&encode_with_no_sign, "1".as_bytes()); + } +} diff --git a/crypto/src/payload_encode_v38.rs b/crypto/src/payload_encode_v38.rs new file mode 100644 index 0000000..bc047fd --- /dev/null +++ b/crypto/src/payload_encode_v38.rs @@ -0,0 +1,409 @@ +use super::aes_gcm::aes_encrypt; +use super::number_util::random_iv; +use super::post_encryption::EncryptionResultE2E; +use super::Error; +use bitcoin::secp256k1::{ecdh, PublicKey, SecretKey}; +use std::collections::HashMap; + +impl From for Error { + fn from(_err: bitcoin::secp256k1::Error) -> Error { + Error::InvalidPrivateKey + } +} + +use base64::{decode_config, encode_config, STANDARD, URL_SAFE_NO_PAD}; + +const SHARED_KEY_ENCODED: &str = "3Bf8BJ3ZPSMUM2jg2ThODeLuRRD_-_iwQEaeLdcQXpg"; +const E2E_KEY: [u8; 2] = [40, 70]; +const E2E_IV: [u8; 1] = [33]; + +enum Index { + AuthorPublicKey = 5, + PublicShared = 6, + AuthorIdentifier = 7, +} + +pub fn encode_v38( + is_public: bool, + network: &str, + author_id: Option<&str>, + iv: &[u8], + key: &[u8], + encrypted: &[u8], + author_pub_key: Option<&[u8]>, + local_key_data: Option<&[u8]>, + target: HashMap>, + author_private_key: Option<&[u8]>, +) -> Result<(String, Option>), Error> { + let base64_config = STANDARD; + let (aes_key_encrypted, ecdh_result): (String, Option>) = + match is_public { + true => (encode_aes_key_encrypted(iv, key)?, None), + false => { + let local_key = local_key_data.ok_or(Error::InvalidLocalKey)?; + let post_key_encoded = encode_post_key(key); + let owners_aes_key_encrypted = + encrypt_by_local_key(&post_key_encoded, iv, local_key)?; + let author_private_key_data = author_private_key.ok_or(Error::InvalidPrivateKey)?; + let ecdh_result = + add_receiver(author_private_key_data, &target, &post_key_encoded)?; + let owners_aes_key_encrypted_string = + encode_config(&owners_aes_key_encrypted, base64_config); + (owners_aes_key_encrypted_string, Some(ecdh_result)) + } + }; + + let encoded_iv = encode_config(&iv, base64_config); + let encoded_encrypted = encode_config(&encrypted, base64_config); + let signature = "_"; + let encoded_fields = encode_fields( + is_public, + &aes_key_encrypted, + &encoded_iv, + &encoded_encrypted, + signature, + network, + author_id, + author_pub_key, + )?; + Ok((encoded_fields, ecdh_result)) +} + +fn encode_aes_key_encrypted(iv: &[u8], key: &[u8]) -> Result { + let base64_url_config = URL_SAFE_NO_PAD; + let encoded_aes_key = encode_config(&key, base64_url_config); + let ab = format!( + r#"{{"alg":"A256GCM","ext":true,"k":"{}","key_ops":["decrypt","encrypt"],"kty":"oct"}}"#, + &encoded_aes_key + ); + let ab_bytes = ab.as_bytes(); + let shared_key_bytes = decode_config(&SHARED_KEY_ENCODED, base64_url_config) + .map_err(|_| Error::InvalidCiphertext)?; + let encrypted_key = aes_encrypt(iv, &shared_key_bytes, ab_bytes)?; + let base64_config = STANDARD; + let encoded_key = encode_config(&encrypted_key, base64_config); + Ok(encoded_key) +} + +fn encrypt_by_local_key( + encoded_post_key: &[u8], + post_iv: &[u8], + local_key_data: &[u8], +) -> Result, Error> { + aes_encrypt(post_iv, local_key_data, encoded_post_key) +} + +fn encode_fields( + is_public: bool, + aes_key_encrypted: &str, + encoded_iv: &str, + encoded_encrypted: &str, + signature: &str, + network: &str, + author_id: Option<&str>, + author_pub_key: Option<&[u8]>, +) -> Result { + let mut fields: [&str; 8] = [ + "\u{1F3BC}4/4", + aes_key_encrypted, + encoded_iv, + encoded_encrypted, + signature, + "", + "", + "", + ]; + + let public_key_str = match author_pub_key { + Some(key_data) => { + let base64_config = STANDARD; + let public_key = + PublicKey::from_slice(key_data).map_err(|_| Error::InvalidPrivateKey)?; + let compressed_key = public_key.serialize(); + encode_config(&compressed_key, base64_config) + } + None => "".to_string(), + }; + fields[Index::AuthorPublicKey as usize] = &public_key_str; + + match is_public { + true => { + fields[Index::PublicShared as usize] = "1"; + } + false => { + fields[Index::PublicShared as usize] = "0"; + } + } + + let identity = match author_id { + Some(author_id) => { + let profile_identifier = format!("{}/{}", network, author_id); + let base64_config = STANDARD; + encode_config(&profile_identifier, base64_config) + } + _ => "".to_string(), + }; + fields[Index::AuthorIdentifier as usize] = &identity; + + let joined_fields = fields.join("|"); + let result = format!("{}:||", joined_fields); + Ok(result) +} + +fn encode_post_key(post_key: &[u8]) -> Vec { + let base64_url_config = URL_SAFE_NO_PAD; + let encoded_post_key = encode_config(&post_key, base64_url_config); + let result = format!( + r#"{{"alg":"A256GCM","ext":true,"k":"{}","key_ops":["decrypt","encrypt"],"kty":"oct"}}"#, + &encoded_post_key + ); + result.as_bytes().to_vec() +} + +fn add_receiver( + author_private_key: &[u8], + target: &HashMap>, + encoded_post_key: &[u8], +) -> Result, Error> { + let mut ecdh_result = HashMap::new(); + for (profile_id, receiver_public_key) in target.iter() { + let iv_to_be_published = random_iv(16); + let (aes, iv) = derive_ecdh_and_extra_steps( + receiver_public_key, + author_private_key, + &iv_to_be_published, + )?; + let encrypted_post_key = aes_encrypt(&iv, &aes, encoded_post_key)?; + let result = EncryptionResultE2E { + target: profile_id.to_string(), + iv_to_be_published: Some(iv_to_be_published), + encrypted_post_key, + }; + ecdh_result.insert(profile_id.to_string(), result); + } + Ok(ecdh_result) +} + +fn derive_ecdh_and_extra_steps( + public_key: &[u8], + author_private_key: &[u8], + iv: &[u8], +) -> Result<(Vec, [u8; 16]), Error> { + use sha2::{Digest, Sha256}; + let derive_result = derive_aes_by_ecdh(public_key, author_private_key)?; + let mut _a = Vec::new(); + _a.extend(&derive_result); + _a.extend(iv); + + let mut next_key_material_raw = Vec::new(); + next_key_material_raw.extend(&_a); + next_key_material_raw.extend(iv); + next_key_material_raw.extend(E2E_KEY); + let next_aes_key_material = Sha256::digest(&next_key_material_raw); + + let mut iv_pre_raw = Vec::new(); + iv_pre_raw.extend(&_a); + iv_pre_raw.extend(iv); + iv_pre_raw.extend(E2E_IV); + let iv_pre = Sha256::digest(&iv_pre_raw); + + let mut next_iv: [u8; 16] = [0; 16]; + for i in 0..16 { + next_iv[i] = iv_pre[i] ^ iv_pre[16 + i]; + } + Ok((next_aes_key_material.to_vec(), next_iv)) +} + +fn derive_aes_by_ecdh(public_key: &[u8], private_key: &[u8]) -> Result, Error> { + let pub_key = PublicKey::from_slice(public_key)?; + let sec_key = SecretKey::from_slice(private_key)?; + let shared_secret = ecdh::SharedSecret::new(&pub_key, &sec_key); + Ok(shared_secret.as_ref().to_vec()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aes_gcm::aes_decrypt; + use crate::number_util::random_iv; + use sha2::{Digest, Sha256}; + #[test] + fn test_encode_aes_key() { + let iv = [ + 44, 67, 220, 0, 135, 88, 111, 139, 0, 72, 96, 128, 156, 163, 95, 183, + ]; + let key_iv = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + + let encoded_key = encode_aes_key_encrypted(&iv, &key_iv).unwrap(); + assert_eq!(encoded_key, "7jLN2yGxMEVM28cIjVlJJ1PBSh6qt3qgUoDL579dssh4EQoxImWZfezILlxTMtoPEFzIN8T369jz2Pai2IzrI9coSAr+V46S91/4Bh2QnlSsWc6B+IZIc/hIWhFKBUeU+5bq/SvBYSVpE5/+C4sIk8beyHIl"); + } + + #[test] + fn test_encode_fields() { + let aes_key_encrypted = "8NXbnHHTwNaQlnihC4ov7JiAXIMfjmpP6LZG9SCpsBTGgscJuET25HO0DfkXOmjtepWV5NAGzRn5iFJENjTtIeMmnAaDl7ijSmsIfcS6Gp9wQZZ2yUaAj4S1rN6zCx6uZQNPaVH2kywLfQVZJ+pxNflXmKYgNcw53yG/XKgI7ksqCnwWqiqQyYYS"; + let encoded_iv = "Q43qdWbDoDbWBca2+LB6lA=="; + let encoded_encrypted = "h6WdGLrQ+H2fMJrXVFwKFw+IiQ=="; + let signature = "_"; + let network = "twitter.com"; + let author_id = "yuan_brad"; + let public_key_data = [ + 2, 210, 107, 119, 140, 57, 180, 37, 245, 126, 86, 79, 41, 128, 107, 64, 99, 141, 222, + 6, 87, 249, 95, 130, 198, 99, 1, 113, 41, 91, 239, 152, 212, + ]; + + let encoded_fields = encode_fields( + true, + &aes_key_encrypted, + &encoded_iv, + &encoded_encrypted, + &signature, + network, + Some(author_id), + Some(&public_key_data), + ) + .unwrap(); + assert_eq!(encoded_fields, "🎼4/4|8NXbnHHTwNaQlnihC4ov7JiAXIMfjmpP6LZG9SCpsBTGgscJuET25HO0DfkXOmjtepWV5NAGzRn5iFJENjTtIeMmnAaDl7ijSmsIfcS6Gp9wQZZ2yUaAj4S1rN6zCx6uZQNPaVH2kywLfQVZJ+pxNflXmKYgNcw53yG/XKgI7ksqCnwWqiqQyYYS|Q43qdWbDoDbWBca2+LB6lA==|h6WdGLrQ+H2fMJrXVFwKFw+IiQ==|_|AtJrd4w5tCX1flZPKYBrQGON3gZX+V+CxmMBcSlb75jU|1|dHdpdHRlci5jb20veXVhbl9icmFk:||"); + } + + #[test] + fn test_ecdh_derive() { + // Check whether the `derive_aes_by_ecdh` method could derive a valid aes key + let test_message = "hello world"; + let test_iv = random_iv(16); + let private_key = vec![ + 164, 220, 24, 245, 162, 159, 141, 176, 18, 151, 248, 162, 174, 140, 138, 146, 6, 126, + 21, 156, 237, 185, 200, 177, 167, 250, 42, 150, 246, 13, 30, 134, + ]; + let public_key = vec![ + 2, 170, 10, 30, 27, 232, 4, 43, 63, 50, 63, 249, 34, 255, 147, 179, 179, 85, 203, 103, + 115, 52, 111, 166, 140, 56, 20, 223, 54, 25, 143, 49, 28, + ]; + assert_eq!(PublicKey::from_slice(&public_key).is_ok(), true); + assert_eq!(SecretKey::from_slice(&private_key).is_ok(), true); + let shared_secret = derive_aes_by_ecdh(&public_key, &private_key).unwrap(); + let encrypted = aes_encrypt(&test_iv, &shared_secret, test_message.as_bytes()).unwrap(); + let decrypted = aes_decrypt(&test_iv, &shared_secret, &encrypted).unwrap(); + assert_eq!(decrypted, test_message.as_bytes()); + } + + #[test] + fn test_derive_ecdh_and_extra_steps() { + let derived_key_raw = [ + 62, 155, 237, 84, 13, 3, 137, 47, 239, 227, 65, 3, 107, 135, 75, 66, 118, 27, 77, 132, + 91, 79, 223, 58, 248, 249, 95, 193, 42, 88, 199, 12, + ]; + let iv = [ + 148, 238, 119, 124, 75, 191, 117, 180, 14, 0, 77, 65, 63, 213, 1, 227, + ]; + let mut _a = Vec::new(); + _a.extend(&derived_key_raw); + _a.extend(iv); + + assert_eq!( + &_a, + &[ + 62, 155, 237, 84, 13, 3, 137, 47, 239, 227, 65, 3, 107, 135, 75, 66, 118, 27, 77, + 132, 91, 79, 223, 58, 248, 249, 95, 193, 42, 88, 199, 12, 148, 238, 119, 124, 75, + 191, 117, 180, 14, 0, 77, 65, 63, 213, 1, 227 + ] + ); + + let mut next_key_material_raw = Vec::new(); + next_key_material_raw.extend(&_a); + next_key_material_raw.extend(iv); + next_key_material_raw.extend(E2E_KEY); + let next_aes_key_material = Sha256::digest(&next_key_material_raw); + assert_eq!( + &next_aes_key_material.to_vec(), + &[ + 74, 12, 205, 110, 243, 104, 194, 172, 14, 90, 45, 147, 214, 168, 127, 97, 242, 39, + 56, 126, 197, 0, 228, 66, 97, 6, 86, 132, 38, 76, 166, 24 + ] + ); + + let mut iv_pre_raw = Vec::new(); + iv_pre_raw.extend(&_a); + iv_pre_raw.extend(iv); + iv_pre_raw.extend(E2E_IV); + let iv_pre = Sha256::digest(&iv_pre_raw); + + assert_eq!( + &iv_pre.to_vec(), + &[ + 194, 208, 143, 170, 171, 60, 198, 137, 133, 142, 75, 252, 168, 127, 65, 229, 41, + 208, 41, 99, 233, 19, 118, 190, 203, 252, 150, 137, 221, 215, 144, 68 + ] + ); + + let mut next_iv: [u8; 16] = [0; 16]; + for i in 0..16 { + next_iv[i] = iv_pre[i] ^ iv_pre[16 + i]; + } + + assert_eq!( + &next_iv, + &[235, 0, 166, 201, 66, 47, 176, 55, 78, 114, 221, 117, 117, 168, 209, 161] + ); + } + + #[test] + fn test_encode_v38() { + let base64_url_config = URL_SAFE_NO_PAD; + let is_public = false; + let network = "twitter.com"; + let author_id = "yuan_brad"; + let iv = [ + 8, 224, 216, 3, 117, 23, 198, 40, 218, 134, 149, 179, 52, 216, 88, 91, + ]; + let aes_key_encoded = "MERv1-yBnsotcyzNG5zHv6WFlfIkGeosp2-UA1U_1Io"; + let aes_key = decode_config(&aes_key_encoded, base64_url_config).unwrap(); + let encrypted_message = [ + 178, 39, 43, 146, 217, 20, 125, 160, 36, 78, 54, 45, 100, 113, 253, 43, 49, 165, 202, + 106, 185, 155, + ]; + + let author_pub_key_x_str = "LQ37fyhD6ug-2a9xmlez8bD3_eNTQnZ2O_8lRcWNSI4"; + let author_pub_key_y_str = "yoWbbZIyR-8dwLivurXT4fwD3QqP4sZ329jan3fp4I0"; + let author_pub_key_x_1 = decode_config(&author_pub_key_x_str, base64_url_config).unwrap(); + let author_pub_key_y_1 = decode_config(&author_pub_key_y_str, base64_url_config).unwrap(); + let author_public_key = [[0x04].to_vec(), author_pub_key_x_1, author_pub_key_y_1].concat(); + + let target_pub_key_x_str = "j3RDjs8gfSBG2kpn5oX67e7CioZxRM1k1uyx7UzHpVU"; + let target_pub_key_y_str = "Q68JL9-pStMOzi3BlM8N8tAkiIY4PZqO7tvDk19sTm0"; + let target_pub_key_x_1 = decode_config(&target_pub_key_x_str, base64_url_config).unwrap(); + let target_pub_key_y_1 = decode_config(&target_pub_key_y_str, base64_url_config).unwrap(); + let target_public_key = [[0x04].to_vec(), target_pub_key_x_1, target_pub_key_y_1].concat(); + assert_eq!(target_public_key.len(), 65); + + let local_key_str = "JzGZnwVX9RKdkAKsrWmNMnzixUZA8I7vaaa2T_tEIT0"; + let local_key = decode_config(&local_key_str, base64_url_config).unwrap(); + let author_private_key_str = "xx96FEmD0_syCDgTu9vZW7doi8dFDwKe59P-a_N2jTg"; + + let author_private_key_data = + decode_config(&author_private_key_str, base64_url_config).unwrap(); + + let sec_key = SecretKey::from_slice(&author_private_key_data).unwrap(); + let pub_key = PublicKey::from_slice(&target_public_key).unwrap(); + let shared_secret = ecdh::SharedSecret::new(&pub_key, &sec_key); + println!("{:?}", &shared_secret); + + let mut target: HashMap> = HashMap::new(); + target.insert("author_id".to_string(), target_public_key); + + let (output, e2e_result) = encode_v38( + is_public, + network, + Some(author_id), + &iv, + &aes_key, + &encrypted_message, + Some(&author_public_key), + Some(&local_key), + target, + Some(&author_private_key_data), + ) + .unwrap(); + println!("{:?}", e2e_result); + assert_eq!(output, "🎼4/4|Bwpu5LcIkJkW2IWz1FJSXjso2l312ydbACk0owMXFXC2VUci0I7dK7smPEW/iAXU0v0b6pttFOdPsavNUJl+CSkjHaeKY4pBGdRPVLVX9wTFvha7233bTAh7H8MaOQKAcjMTTPSpiIfXV6z+adQ4ub/GBz13JEEcq1tBWGe14e6KJM0BAlavKA8W|CODYA3UXxijahpWzNNhYWw==|sicrktkUfaAkTjYtZHH9KzGlymq5mw==|_|Ay0N+38oQ+roPtmvcZpXs/Gw9/3jU0J2djv/JUXFjUiO|0|dHdpdHRlci5jb20veXVhbl9icmFk:||"); + } +} diff --git a/crypto/src/post_encryption.rs b/crypto/src/post_encryption.rs new file mode 100644 index 0000000..a9fd6c1 --- /dev/null +++ b/crypto/src/post_encryption.rs @@ -0,0 +1,138 @@ +use super::number_util::random_iv; +// use super::payload_encode_v37::encode_with_container as encode_v37; +use super::payload_encode_v38::encode_v38; +use super::Error; +use base64::{encode_config, STANDARD}; +use std::collections::HashMap; + +use super::aes_gcm::aes_encrypt; + +use std::str; + +const IV_SIZE: usize = 16; +const AES_KEY_SIZE: usize = 32; + +pub enum Version { + V37 = -37, + V38 = -38, +} + +#[derive(Debug)] +pub struct EncryptionResultE2E { + pub target: String, + pub encrypted_post_key: Vec, + pub iv_to_be_published: Option>, +} + +pub struct EncryptionResult { + pub output: String, + pub post_key: Vec, + pub post_identifier: String, + pub e2e_result: Option>, +} + +pub fn encrypt( + version: Version, + is_public: bool, + network: &str, + author_id: Option<&str>, + _algr: Option, + author_pub_key: Option<&[u8]>, + message: &[u8], + local_key_data: Option<&[u8]>, + target: HashMap>, + author_private_key: Option<&[u8]>, +) -> Result { + let post_iv = random_iv(IV_SIZE); + let post_key_iv = random_iv(AES_KEY_SIZE); + + let encrypted_message = aes_encrypt(&post_iv, &post_key_iv, message)?; + + let result = match version { + Version::V37 => Err(Error::NotSupportedCipher), + Version::V38 => encode_v38( + is_public, + network, + author_id, + &post_iv, + &post_key_iv, + &encrypted_message, + author_pub_key, + local_key_data, + target, + author_private_key, + ) + .map_err(|_| Error::InvalidCiphertext), + }?; + + let encoded_post_iv = encode_config(&post_iv, STANDARD).replace("/", "|"); + let post_identifier = format!("post_iv:{}/{}", &network, &encoded_post_iv); + + Ok(EncryptionResult { + output: result.0, + post_key: post_key_iv, + post_identifier: post_identifier, + e2e_result: result.1, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::aes_gcm::aes_decrypt; + use rmp::encode::*; + // content text: "sample text" + const ENCODED_MESSAGE: [u8; 18] = [ + 146, 0, 148, 1, 1, 192, 171, 115, 97, 109, 112, 108, 101, 32, 116, 101, 120, 116, + ]; + + #[test] + fn test_encoding() { + let mut buf = Vec::new(); + write_array_len(&mut buf, 2).unwrap(); + write_sint(&mut buf, 0).unwrap(); + write_array_len(&mut buf, 4).unwrap(); + write_sint(&mut buf, 1).unwrap(); + write_sint(&mut buf, 1).unwrap(); + write_nil(&mut buf).unwrap(); + write_str(&mut buf, "sample text").unwrap(); + println!("{:?}", &buf[..]); + assert_eq!(&buf[..], &ENCODED_MESSAGE); + } + + #[test] + fn test_aes_encrypt() { + let iv: [u8; 16] = [1; 16]; + let key: [u8; 32] = [2; 32]; + let content = "sample text"; + let encrypted = aes_encrypt(&iv, &key, content.as_bytes()).unwrap(); + let decrypted = aes_decrypt(&iv, &key, &encrypted).unwrap(); + assert_eq!(decrypted, content.as_bytes()); + } + + #[test] + fn test_encrypt_v38_public() { + let network = "twitter.com"; + let author_id = "yuan_brad"; + let message = "123"; + let algr = 2; + let public_key_data = [ + 2, 210, 107, 119, 140, 57, 180, 37, 245, 126, 86, 79, 41, 128, 107, 64, 99, 141, 222, + 6, 87, 249, 95, 130, 198, 99, 1, 113, 41, 91, 239, 152, 212, + ]; + // let output = encrypt( + let encryption_result = encrypt( + Version::V38, + true, + network, + Some(author_id), + Some(algr), + Some(&public_key_data), + message.as_bytes(), + None, + HashMap::new(), + None, + ) + .unwrap(); + } +} diff --git a/interface/src/handler.rs b/interface/src/handler.rs index 145bef2..b7e1c5c 100644 --- a/interface/src/handler.rs +++ b/interface/src/handler.rs @@ -1,5 +1,6 @@ mod account; mod common; +mod encryption; mod persona; mod sign; mod stored_key; @@ -37,5 +38,7 @@ pub fn dispatch_request(request: mw_request::Request) -> MwResponse { ParamGenerateMnemonic(_) => common::generate_mnemonic(), ParamGeneratePersona(param) => persona::generate_persona(¶m), + + ParamPostEncryption(param) => encryption::encode(param), } } diff --git a/interface/src/handler/encryption.rs b/interface/src/handler/encryption.rs new file mode 100644 index 0000000..86f8703 --- /dev/null +++ b/interface/src/handler/encryption.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; + +use crypto::post_encryption::{encrypt, Version}; + +use chain_common::api::{ + mw_response::Response, E2eEncryptionResult, MwResponse, PostEncryptedResp, PostEncryptionParam, +}; + +pub fn encode(param: PostEncryptionParam) -> MwResponse { + let version = match param.version { + 0 => Version::V37, + 1 => Version::V38, + _ => Version::V38, + }; + let algr = param.author_public_key_algr.map(|f| f as u8); + let local_key = param.param.as_ref().map(|x| x.local_key_data.clone()); + let author_private_key = param.param.as_ref().map(|x| x.author_private_key.clone()); + let target = param.param.map_or(HashMap::new(), |x| x.target); + let result = encrypt( + version, + param.is_plublic, + ¶m.network, + param.author_user_id.as_deref(), + algr, + param.author_public_key_data.as_deref(), + param.content.as_bytes(), + local_key.as_deref(), + target, + author_private_key.as_deref(), + ); + + match result { + Ok(encryption_result) => { + // TODO: finish implementation + let content = PostEncryptedResp { + content: encryption_result.output, + results: encryption_result + .e2e_result + .unwrap_or_default() + .into_iter() + .map(|(k, v)| { + ( + k, + E2eEncryptionResult { + iv: v.iv_to_be_published, + encrypted_post_key_data: v.encrypted_post_key, + ephemeral_public_key_data: None, + }, + ) + }) + .collect(), + post_identifier: encryption_result.post_identifier, + post_key: encryption_result.post_key, + }; + Response::RespPostEncryption(content).into() + } + + Err(err) => err.into(), + } +} diff --git a/interface/src/handler/persona.rs b/interface/src/handler/persona.rs index e7d8cb4..36ea977 100644 --- a/interface/src/handler/persona.rs +++ b/interface/src/handler/persona.rs @@ -1,9 +1,8 @@ use std::convert::TryInto; use chain_common::api::{ - encrypt_option::Version, mw_response::Response, persona_generation_param::Curve, AesJwkResp, - EncryptOption, JwkResp, MwResponse, MwResponseError, PersonaGenerationParam, - PersonaGenerationResp, + encrypt_option::Version, mw_response::Response, AesJwk, Curve, EncryptOption, Jwk, MwResponse, + MwResponseError, PersonaGenerationParam, PersonaGenerationResp, }; use crypto::{jwk::AesJWK, jwk::JWK, pbkdf2, Error}; @@ -60,8 +59,8 @@ impl JWKWrapper { let private_key = self.as_private_key(); let public_key = self.as_public_key(); - let local_key_resp: Option = match local_key { - Some(aes_jwk) => Some(AesJwkResp { + let local_key_resp: Option = match local_key { + Some(aes_jwk) => Some(AesJwk { alg: aes_jwk.alg, ext: aes_jwk.ext, k: aes_jwk.k, @@ -80,8 +79,8 @@ impl JWKWrapper { } } - fn as_public_key(&self) -> JwkResp { - JwkResp { + fn as_public_key(&self) -> Jwk { + Jwk { crv: self.0.crv.clone(), ext: self.0.ext, x: self.0.x.clone(), @@ -92,8 +91,8 @@ impl JWKWrapper { } } - fn as_private_key(&self) -> JwkResp { - JwkResp { + fn as_private_key(&self) -> Jwk { + Jwk { crv: self.0.crv.clone(), ext: self.0.ext, x: self.0.x.clone(), diff --git a/interface/src/handler/stored_key.rs b/interface/src/handler/stored_key.rs index e34cdfe..e2d6ed5 100644 --- a/interface/src/handler/stored_key.rs +++ b/interface/src/handler/stored_key.rs @@ -378,6 +378,6 @@ mod tests { .into_iter() .map(|r#type| r#type as i32) .collect(); - assert_eq!(types, vec![0, 1, 2]); + assert_eq!(types, vec![1, 0, 2]); } }