From c317a80119d3d067c3c2d1448703a2ecfc6527e7 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sat, 30 Sep 2023 00:23:20 +0800 Subject: [PATCH] feat: ecdh encrypt --- Cargo.lock | 152 +++++---------------------------------------- Cargo.toml | 4 +- src/cmd_encrypt.rs | 84 +++++++------------------ src/crypto_aes.rs | 16 +++-- src/main.rs | 1 + src/util.rs | 7 +++ src/util_ecdh.rs | 34 ++++++++++ 7 files changed, 92 insertions(+), 206 deletions(-) create mode 100644 src/util_ecdh.rs diff --git a/Cargo.lock b/Cargo.lock index 46335ba..97646a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,16 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - [[package]] name = "aes" version = "0.7.5" @@ -51,21 +41,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "aes-gcm" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" -dependencies = [ - "aead", - "aes 0.8.3", - "cipher 0.4.4", - "ctr", - "ghash", - "subtle", - "zeroize", -] - [[package]] name = "aes-gcm-stream" version = "0.2.0" @@ -78,15 +53,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "aho-corasick" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" -dependencies = [ - "memchr", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -160,7 +126,7 @@ dependencies = [ "asn1-rs-impl", "displaydoc", "nom", - "num-traits 0.2.16", + "num-traits", "rusticata-macros", "thiserror", "time", @@ -338,7 +304,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", - "num-traits 0.2.16", + "num-traits", "wasm-bindgen", "windows-targets", ] @@ -467,7 +433,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", "typenum", ] @@ -481,15 +446,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "data-encoding" version = "2.4.0" @@ -519,7 +475,7 @@ dependencies = [ "displaydoc", "nom", "num-bigint", - "num-traits 0.2.16", + "num-traits", "rusticata-macros", ] @@ -1115,16 +1071,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" -dependencies = [ - "serde 0.8.23", - "serde_test", -] - [[package]] name = "linux-raw-sys" version = "0.4.5" @@ -1211,7 +1157,7 @@ checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", - "num-traits 0.2.16", + "num-traits", ] [[package]] @@ -1225,9 +1171,9 @@ dependencies = [ "libm", "num-integer", "num-iter", - "num-traits 0.2.16", + "num-traits", "rand", - "serde 1.0.188", + "serde", "smallvec", "zeroize", ] @@ -1239,7 +1185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", - "num-traits 0.2.16", + "num-traits", ] [[package]] @@ -1250,16 +1196,7 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg", "num-integer", - "num-traits 0.2.16", -] - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -dependencies = [ - "num-traits 0.2.16", + "num-traits", ] [[package]] @@ -1620,35 +1557,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "regex" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "reqwest" version = "0.11.20" @@ -1676,7 +1584,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile", - "serde 1.0.188", + "serde", "serde_json", "serde_urlencoded", "tokio", @@ -1739,7 +1647,7 @@ dependencies = [ "num-bigint-dig", "num-integer", "num-iter", - "num-traits 0.2.16", + "num-traits", "pkcs1", "pkcs8", "rand_core", @@ -1918,12 +1826,6 @@ dependencies = [ "libc", ] -[[package]] -name = "serde" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" - [[package]] name = "serde" version = "1.0.188" @@ -1933,19 +1835,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-hjson" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" -dependencies = [ - "lazy_static", - "linked-hash-map", - "num-traits 0.1.43", - "regex", - "serde 0.8.23", -] - [[package]] name = "serde_derive" version = "1.0.188" @@ -1965,16 +1854,7 @@ checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", - "serde 1.0.188", -] - -[[package]] -name = "serde_test" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5" -dependencies = [ - "serde 0.8.23", + "serde", ] [[package]] @@ -1986,7 +1866,7 @@ dependencies = [ "form_urlencoded", "itoa", "ryu", - "serde 1.0.188", + "serde", ] [[package]] @@ -2237,7 +2117,7 @@ checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", - "serde 1.0.188", + "serde", "time-core", "time-macros", ] @@ -2261,7 +2141,6 @@ dependencies = [ name = "tiny-encrypt" version = "0.0.3" dependencies = [ - "aes-gcm", "aes-gcm-stream", "base64", "chrono", @@ -2276,8 +2155,7 @@ dependencies = [ "rpassword", "rsa", "rust_util", - "serde 1.0.188", - "serde-hjson", + "serde", "serde_json", "sha256", "simpledateformat", @@ -2743,7 +2621,7 @@ dependencies = [ "nom", "num-bigint-dig", "num-integer", - "num-traits 0.2.16", + "num-traits", "p256", "p384", "pbkdf2", diff --git a/Cargo.toml b/Cargo.toml index c18a5cc..5884710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ description = "A simple and tiny file encrypt tool" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -aes-gcm = { version = "0.10.1", features = ["zeroize"] } aes-gcm-stream = "0.2.0" base64 = "0.21.0" chrono = "0.4.23" @@ -17,14 +16,13 @@ flate2 = "1.0.27" hex = "0.4.3" openpgp-card = "0.3.7" openpgp-card-pcsc = "0.3.0" -p256 = { version = "0.13.2", features = ["pem", "ecdh"] } +p256 = { version = "0.13.2", features = ["pem", "ecdh", "pkcs8"] } rand = "0.8.5" reqwest = { version = "0.11.14", features = ["blocking", "rustls", "rustls-tls"] } rpassword = "7.2.0" rsa = { version = "0.9.2", features = ["pem"] } rust_util = "0.6.42" serde = { version = "1.0.152", features = ["derive"] } -serde-hjson = "0.9.1" serde_json = "1.0.93" sha256 = "1.4.0" simpledateformat = "0.1.4" diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs index f0eb577..084d84a 100644 --- a/src/cmd_encrypt.rs +++ b/src/cmd_encrypt.rs @@ -2,18 +2,16 @@ use std::fs; use std::path::PathBuf; use clap::Args; -use p256::{PublicKey, EncodedPoint}; -use p256::ecdh::EphemeralSecret; -use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; -use rand::random; -use rand::rngs::OsRng; use rsa::Pkcs1v15Encrypt; use rust_util::{debugging, failure, opt_result, simple_error, success, XResult}; use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop}; +use crate::crypto_aes::aes_gcm_encrypt; use crate::crypto_rsa::parse_spki; use crate::spec::{EncMetadata, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; -use crate::util::{encode_base64, simple_kdf, TINY_ENC_CONFIG_FILE}; +use crate::util::{ENC_AES256_GCM_P256, encode_base64, make_key256_and_nonce, simple_kdf, TINY_ENC_CONFIG_FILE, zeroize}; +use crate::util_ecdh::compute_shared_secret; +use crate::wrap_key::{WrapKey, WrapKeyHeader}; #[derive(Debug, Args)] pub struct CmdEncrypt { @@ -54,9 +52,12 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop]) -> XRe compress: false, }; - let _encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce, envelops); + let encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce, envelops); + debugging!("Encrypted meta: {:?}", encrypt_meta); // TODO write to file and do encrypt + zeroize(key); + zeroize(nonce); Ok(()) } @@ -78,49 +79,31 @@ fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResu fn encrypt_envelop_ecdh(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult { let public_key_point_hex = &envelop.public_part; - let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}"); - let encoded_point = opt_result!(EncodedPoint::from_bytes(&public_key_point_bytes), "Parse public key point failed: {}"); - let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap(); + let (shared_secret, ephemeral_spki) = compute_shared_secret(public_key_point_hex)?; + let shared_key = simple_kdf(shared_secret.as_slice()); + let (_, nonce) = make_key256_and_nonce(); - let esk = EphemeralSecret::random(&mut OsRng); - let epk = esk.public_key(); - let epk_bytes = EphemeralKeyBytes::from_public_key(&epk); - let public_key_encoded_point = public_key.to_encoded_point(false); - let shared_secret = esk.diffie_hellman(&public_key); - let key = simple_kdf(shared_secret.raw_secret_bytes().as_slice()); + let encrypted_key = aes_gcm_encrypt(&shared_key, &nonce, key)?; - // PORT Java Implementation - // public static WrapKey encryptEcdhP256(String kid, PublicKey publicKey, byte[] data) { - // AssertUtil.isTrue(publicKey instanceof ECPublicKey, "Public key must be EC public key"); - // if (data == null || data.length == 0) { - // return null; - // } - // final Tuple2 ecdh = ECUtil.ecdh(ECUtil.CURVE_SECP256R1, publicKey); - // final byte[] ePublicKeyBytes = ecdh.getVal1().getEncoded(); - // final byte[] key = KdfUtil.simpleKdf256(ecdh.getVal2()); - // - // final byte[] nonce = RandomTool.secureRandom().nextbytes(AESCryptTool.GCM_NONCE_LENGTH); - // final byte[] encryptedData = AESCryptTool.gcmEncrypt(key, nonce).from(Bytes.from(data)).toBytes().bytes(); - // final WrapKey wrapKey = new WrapKey(); - // final WrapKeyHeader wrapKeyHeader = new WrapKeyHeader(); - // wrapKeyHeader.setKid(kid); - // wrapKeyHeader.setEnc(ENC_AES256_GCM_P256); - // wrapKeyHeader.setePubKey(Base64s.uriCompatible().encode(ePublicKeyBytes)); - // wrapKey.setHeader(wrapKeyHeader); - // wrapKey.setNonce(nonce); - // wrapKey.setEncrytpedData(encryptedData); - // return wrapKey; - // } + let wrap_key = WrapKey { + header: WrapKeyHeader { + kid: Some(envelop.kid.clone()), + enc: ENC_AES256_GCM_P256.to_string(), + e_pub_key: encode_base64(&ephemeral_spki), + }, + nonce, + encrypted_data: encrypted_key, + }; + let encoded_wrap_key = wrap_key.encode()?; Ok(TinyEncryptEnvelop { r#type: envelop.r#type, kid: envelop.kid.clone(), desc: envelop.desc.clone(), - encrypted_key: "".to_string(), // TODO ... + encrypted_key: encoded_wrap_key, }) } - fn encrypt_envelop_pgp(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult { let pgp_public_key = opt_result!(parse_spki(&envelop.public_part), "Parse PGP public key failed: {}"); let mut rng = rand::thread_rng(); @@ -132,24 +115,3 @@ fn encrypt_envelop_pgp(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResul encrypted_key: encode_base64(&encrypted_key), }) } - -fn make_key256_and_nonce() -> (Vec, Vec) { - let key: [u8; 32] = random(); - let nonce: [u8; 12] = random(); - (key.into(), nonce.into()) -} - -#[derive(Debug)] -pub struct EphemeralKeyBytes(EncodedPoint); - -impl EphemeralKeyBytes { - fn from_public_key(epk: &PublicKey) -> Self { - EphemeralKeyBytes(epk.to_encoded_point(true)) - } - - fn decompress(&self) -> EncodedPoint { - // EphemeralKeyBytes is a valid compressed encoding by construction. - let p = PublicKey::from_encoded_point(&self.0).unwrap(); - p.to_encoded_point(false) - } -} diff --git a/src/crypto_aes.rs b/src/crypto_aes.rs index 75e5284..8a8bbb3 100644 --- a/src/crypto_aes.rs +++ b/src/crypto_aes.rs @@ -1,4 +1,4 @@ -use aes_gcm_stream::Aes256GcmStreamDecryptor; +use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor}; use rust_util::{opt_result, XResult}; pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult> { @@ -10,10 +10,18 @@ pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult XResult> { + let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}"); + let mut aes256_gcm = Aes256GcmStreamEncryptor::new(key, nonce); + let mut b1 = aes256_gcm.update(message); + let (b2, tag) = aes256_gcm.finalize(); + b1.extend_from_slice(&b2); + b1.extend_from_slice(&tag); + Ok(b1) +} + #[test] fn test_aes_gcm_01() { - use aes_gcm_stream::Aes256GcmStreamEncryptor; - let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap(); let nonce = hex::decode("000102030405060708091011").unwrap(); @@ -44,8 +52,6 @@ fn test_aes_gcm_01() { #[test] fn test_aes_gcm_02() { - use aes_gcm_stream::Aes256GcmStreamDecryptor; - let data_key = hex::decode("aa01020304050607080910111213141516171819202122232425262728293031").unwrap(); let nonce = hex::decode("aa0102030405060708091011").unwrap(); diff --git a/src/main.rs b/src/main.rs index f30cd54..2a4afc0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use crate::cmd_encrypt::CmdEncrypt; use crate::cmd_info::CmdInfo; mod util; +mod util_ecdh; mod compress; mod config; mod spec; diff --git a/src/util.rs b/src/util.rs index 4001e68..a2a6e3f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,6 +4,7 @@ use std::path::Path; use base64::Engine; use base64::engine::general_purpose; +use rand::random; use rust_util::{simple_error, warning, XResult}; use zeroize::Zeroize; @@ -37,6 +38,12 @@ pub fn require_file_not_exists(path: impl AsRef) -> XResult<()> { } } +pub fn make_key256_and_nonce() -> (Vec, Vec) { + let key: [u8; 32] = random(); + let nonce: [u8; 12] = random(); + (key.into(), nonce.into()) +} + pub fn simple_kdf(input: &[u8]) -> Vec { let input = hex::decode(sha256::digest(input)).unwrap(); let input = hex::decode(sha256::digest(input)).unwrap(); diff --git a/src/util_ecdh.rs b/src/util_ecdh.rs new file mode 100644 index 0000000..dbd4be0 --- /dev/null +++ b/src/util_ecdh.rs @@ -0,0 +1,34 @@ +use p256::ecdh::EphemeralSecret; +use rand::rngs::OsRng; +use rust_util::{opt_result, XResult}; + +use p256::pkcs8::EncodePublicKey; +use p256::{EncodedPoint, PublicKey}; +use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; + +#[derive(Debug)] +pub struct EphemeralKeyBytes(EncodedPoint); + +impl EphemeralKeyBytes { + pub fn from_public_key(epk: &PublicKey) -> Self { + EphemeralKeyBytes(epk.to_encoded_point(true)) + } + + pub fn decompress(&self) -> EncodedPoint { + // EphemeralKeyBytes is a valid compressed encoding by construction. + let p = PublicKey::from_encoded_point(&self.0).unwrap(); + p.to_encoded_point(false) + } +} + +pub fn compute_shared_secret(public_key_point_hex: &str) -> XResult<(Vec, Vec)> { + let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}"); + let encoded_point = opt_result!(EncodedPoint::from_bytes(&public_key_point_bytes), "Parse public key point failed: {}"); + let public_key = PublicKey::from_encoded_point(&encoded_point).unwrap(); + + let esk = EphemeralSecret::random(&mut OsRng); + let epk = esk.public_key(); + let shared_secret = esk.diffie_hellman(&public_key); + let epk_public_key_der = opt_result!(epk.to_public_key_der(), "Convert epk to SPKI failed: {}"); + Ok((shared_secret.raw_secret_bytes().as_slice().to_vec(), epk_public_key_der.to_vec())) +} \ No newline at end of file