diff --git a/.gitignore b/.gitignore index e747356..1a962a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .DS_Store /.vscode +/.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 373002d..22f52ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -304,7 +310,9 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.5", ] @@ -666,6 +674,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fancy-duration" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ae60718ae501dca9d27fd0e322683c86a95a1a01fac1807aa2f9b035cc0882" +dependencies = [ + "anyhow", + "lazy_static", + "regex", +] + [[package]] name = "fancy-regex" version = "0.13.0" @@ -1140,6 +1159,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1245,12 +1279,31 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1344,6 +1397,16 @@ dependencies = [ "regex", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1553,14 +1616,17 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", - "base64", + "base64 0.22.1", "blake3", + "chrono", "clap", "csv", "ed25519-dalek", "enum_dispatch", + "fancy-duration", "futures", "hex", + "jsonwebtoken", "rand", "reqwest", "serde", @@ -1610,7 +1676,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", @@ -1709,7 +1775,7 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64", + "base64 0.22.1", "rustls-pki-types", ] @@ -1893,6 +1959,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -2077,10 +2155,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", "serde", "time-core", + "time-macros", ] [[package]] @@ -2089,6 +2169,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 45d96ad..12259a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,15 @@ anyhow = "^1.0" axum = "^0.7.5" base64 = "^0.22.1" blake3 = "^1.5.1" +chrono = "0.4.38" clap = { version = "^4.5.4", features = ["derive"] } csv = "^1.3.0" ed25519-dalek = { version = "^2.1.1", features = ["rand_core"] } enum_dispatch = "0.3.13" +fancy-duration = "0.9.2" futures = "^0.3.30" hex = "^0.4.3" +jsonwebtoken = "9.3.0" rand = "^0.8.5" reqwest = "0.12.5" serde = { version = "^1.0", features = ["derive"] } diff --git a/Makefile b/Makefile index 7d73ee9..7a891aa 100644 --- a/Makefile +++ b/Makefile @@ -100,3 +100,8 @@ run_with_log: # ******** http ******** # make run ARGS="http serve" + +# ******** jwt ******** +# make run ARGS="jwt sign" +# make run_with_log ARGS="jwt sign --sub noah-future --aud noah --exp 7d" +# make run_with_log ARGS="jwt verify --token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJub2FoIiwiZXhwIjoxNzE5ODQyMjg3LCJzdWIiOiJub2FoLWZ1dHVyZSIsImlhdCI6MTcxOTIzNzQ4N30.V0IEykmRp58PT6fQk4_KbnoytltQKdBU4jmlNMyCh8U" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 0511d7e..0457f9c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,6 +7,7 @@ mod base64; mod csv; mod genpass; mod http; +mod jwt; mod text; // pub use csv_opts::{CsvOpts, OutputFormat}; @@ -18,7 +19,7 @@ mod text; // pub use self::genpass::GenPassOpts; // pub use self::http::{HttpServeOpts, HttpSubCommand}; // pub use self::text::{TextSignFormat, TextSignOpts, TextSubcommand, TextVerifyOpts}; -pub use self::{base64::*, csv::*, genpass::*, http::*, text::*}; +pub use self::{base64::*, csv::*, genpass::*, http::*, jwt::*, text::*}; #[derive(Debug, Parser)] #[command(name = "rcli", version, author, about, long_about = None)] @@ -28,7 +29,7 @@ pub struct Opts { } #[derive(Debug, Parser)] -#[enum_dispatch(CmdExecuter)] +#[enum_dispatch(CmdExecutor)] pub enum SubCommand { #[command(name = "csv", about = "Show CSV, or convert CSV to other formats")] Csv(CsvOpts), @@ -40,13 +41,14 @@ pub enum SubCommand { Text(TextSubcommand), #[command(subcommand, about = "HTTP server")] Http(HttpSubCommand), + #[command(subcommand, about = "JWT sign/verify")] + Jwt(JwtSubCommand), } // 会传入文件名 // csv --input filename(xxx.csv) // pub fn verify_input_file(filename: &str) -> Result { // if Path::new(filename).exists() { -// // Ok(filename.to_string()) // Ok(filename.into()) // } else { // Err("File not found") diff --git a/src/cli/base64.rs b/src/cli/base64.rs index c8df8d2..6bff627 100644 --- a/src/cli/base64.rs +++ b/src/cli/base64.rs @@ -3,12 +3,12 @@ use std::{fmt, str::FromStr}; use clap::Parser; use enum_dispatch::enum_dispatch; -use crate::CmdExecuter; +use crate::CmdExecutor; use super::verify_file; #[derive(Debug, Parser)] -#[enum_dispatch(CmdExecuter)] +#[enum_dispatch(CmdExecutor)] pub enum Base64Subcommand { #[command(name = "encode", about = "Encode a string to base64")] Encode(Base64EncodeOpts), @@ -67,7 +67,7 @@ impl fmt::Display for Base64Format { } } -impl CmdExecuter for Base64EncodeOpts { +impl CmdExecutor for Base64EncodeOpts { async fn execute(self) -> anyhow::Result<()> { let mut reader = crate::get_reader(&self.input)?; let ret = crate::process_encode(&mut reader, self.format)?; @@ -76,7 +76,7 @@ impl CmdExecuter for Base64EncodeOpts { } } -impl CmdExecuter for Base64DecodeOpts { +impl CmdExecutor for Base64DecodeOpts { async fn execute(self) -> anyhow::Result<()> { let mut reader = crate::get_reader(&self.input)?; let ret = crate::process_decode(&mut reader, self.format)?; diff --git a/src/cli/csv.rs b/src/cli/csv.rs index 00dc2ce..011a42e 100644 --- a/src/cli/csv.rs +++ b/src/cli/csv.rs @@ -1,7 +1,7 @@ use clap::Parser; use std::{fmt, str::FromStr}; -use crate::{process_csv, CmdExecuter}; +use crate::{process_csv, CmdExecutor}; use super::verify_file; @@ -41,7 +41,7 @@ pub enum OutputFormat { } // region: --- impls -impl CmdExecuter for CsvOpts { +impl CmdExecutor for CsvOpts { async fn execute(self) -> anyhow::Result<()> { let output = if let Some(output) = self.output { output diff --git a/src/cli/genpass.rs b/src/cli/genpass.rs index 4a4d47b..ba69571 100644 --- a/src/cli/genpass.rs +++ b/src/cli/genpass.rs @@ -1,6 +1,6 @@ use clap::Parser; -use crate::CmdExecuter; +use crate::CmdExecutor; use zxcvbn::zxcvbn; #[derive(Debug, Parser)] @@ -26,7 +26,7 @@ pub struct GenPassOpts { pub symbol: bool, } -impl CmdExecuter for GenPassOpts { +impl CmdExecutor for GenPassOpts { async fn execute(self) -> anyhow::Result<()> { let ret = crate::process_genpass( self.length, diff --git a/src/cli/http.rs b/src/cli/http.rs index e22ee0d..b8e2d27 100644 --- a/src/cli/http.rs +++ b/src/cli/http.rs @@ -3,12 +3,12 @@ use std::path::PathBuf; use clap::Parser; use enum_dispatch::enum_dispatch; -use crate::{process_http_serve, CmdExecuter}; +use crate::{process_http_serve, CmdExecutor}; use super::verify_path; #[derive(Debug, Parser)] -#[enum_dispatch(CmdExecuter)] +#[enum_dispatch(CmdExecutor)] pub enum HttpSubCommand { #[command(about = "Serve a directory over HTTP")] Serve(HttpServeOpts), @@ -23,7 +23,7 @@ pub struct HttpServeOpts { } // region: --- impls -impl CmdExecuter for HttpServeOpts { +impl CmdExecutor for HttpServeOpts { async fn execute(self) -> anyhow::Result<()> { process_http_serve(self.dir, self.port).await } diff --git a/src/cli/jwt.rs b/src/cli/jwt.rs new file mode 100644 index 0000000..a5246d8 --- /dev/null +++ b/src/cli/jwt.rs @@ -0,0 +1,77 @@ +use crate::{process_jwt_sign, process_jwt_verify, CmdExecutor}; +use anyhow::Result; +use clap::Parser; +use enum_dispatch::enum_dispatch; + +// region: --- enum and struct +#[derive(Debug, Clone, Parser)] +#[enum_dispatch(CmdExecutor)] +pub enum JwtSubCommand { + #[command(about = "Sign a JWT token")] + Sign(JwtSignOpts), + #[command(about = "Verify a JWT token")] + Verify(JwtVerifyOpts), +} + +#[derive(Debug, Clone, Parser)] +pub struct JwtSignOpts { + /// key to sign the token + #[arg(short, long, default_value = "")] + key: String, + #[arg(short, long)] + /// subject + sub: String, + #[arg(short, long)] + /// audience + aud: String, + /// expiration time + #[arg(short, long, value_parser= parse_exp,default_value="7d")] + exp: usize, +} + +#[derive(Debug, Clone, Parser)] +pub struct JwtVerifyOpts { + #[arg(short, long, default_value = "")] + key: String, + #[arg(short, long)] + token: String, +} +// endregion: --- enum and struct + +// region: --- impls +impl CmdExecutor for JwtSignOpts { + async fn execute(self) -> Result<()> { + let token = process_jwt_sign(&self.key, &self.sub, &self.aud, self.exp)?; + println!("Token: {}", token); + Ok(()) + } +} + +impl CmdExecutor for JwtVerifyOpts { + async fn execute(self) -> Result<()> { + let verified: bool = process_jwt_verify(&self.key, &self.token)?; + println!("Token valid: {}", verified); + Ok(()) + } +} +// endregion: --- impls + +fn parse_exp(exp: &str) -> Result { + match fancy_duration::FancyDuration::::parse(exp) { + Ok(d) => Ok(d.0.as_secs() as usize), + Err(_) => Err(anyhow::anyhow!("invalid unit: {}", exp)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + /// test date time parse + #[test] + fn test_parse_exp() { + assert_eq!(parse_exp("1s").unwrap(), 1); + assert_eq!(parse_exp("1m").unwrap(), 60); + assert_eq!(parse_exp("1h").unwrap(), 60 * 60); + assert_eq!(parse_exp("1d").unwrap(), 60 * 60 * 24); + } +} diff --git a/src/cli/text.rs b/src/cli/text.rs index de31aef..b375aef 100644 --- a/src/cli/text.rs +++ b/src/cli/text.rs @@ -7,12 +7,12 @@ use tokio::fs; use super::{verify_file, verify_path}; use crate::{ get_content, get_reader, process_text_key_generate, process_text_sign, process_text_verify, - CmdExecuter, + CmdExecutor, }; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; #[derive(Debug, Parser)] -#[enum_dispatch(CmdExecuter)] +#[enum_dispatch(CmdExecutor)] pub enum TextSubcommand { #[command( name = "sign", @@ -96,7 +96,7 @@ impl fmt::Display for TextSignFormat { } } -impl CmdExecuter for TextSignOpts { +impl CmdExecutor for TextSignOpts { async fn execute(self) -> anyhow::Result<()> { let mut reader = get_reader(&self.input)?; let key = get_content(&self.key)?; @@ -108,7 +108,7 @@ impl CmdExecuter for TextSignOpts { } } -impl CmdExecuter for TextVerifyOpts { +impl CmdExecutor for TextVerifyOpts { async fn execute(self) -> anyhow::Result<()> { let mut reader = get_reader(&self.input)?; let key = get_content(&self.key)?; @@ -123,7 +123,7 @@ impl CmdExecuter for TextVerifyOpts { } } -impl CmdExecuter for KeyGenerateOpts { +impl CmdExecutor for KeyGenerateOpts { async fn execute(self) -> anyhow::Result<()> { let key = process_text_key_generate(self.format)?; for (k, v) in key { diff --git a/src/lib.rs b/src/lib.rs index 47d201e..841a22a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,6 @@ pub use utils::*; // after rust 1.75, async fn in trait is allowed #[allow(async_fn_in_trait)] #[enum_dispatch] -pub trait CmdExecuter { +pub trait CmdExecutor { async fn execute(self) -> anyhow::Result<()>; } diff --git a/src/main.rs b/src/main.rs index a4ba3ca..48538e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,15 @@ use clap::Parser; -use rcli::{CmdExecuter, Opts}; +use rcli::{CmdExecutor, Opts}; // 因为需要使用 CmdExecutor trait 的 execute 方法, 所以导入 #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); // init tracing let opts = Opts::parse(); opts.cmd.execute().await?; + + // region: --- Before refactoring with enum_dispatch // match opts.cmd { // SubCommand::Csv(opts) => { // process_csv(&opts.input, &opts.output, &opts.format)?; @@ -86,5 +88,7 @@ async fn main() -> anyhow::Result<()> { // } // }, // } + // endregion: --- Before refactoring with enum_dispatch + Ok(()) } diff --git a/src/process.rs b/src/process.rs index 2b6d1b1..82c8ba1 100644 --- a/src/process.rs +++ b/src/process.rs @@ -2,10 +2,12 @@ mod b64; mod csv_convert; mod gen_pass; mod http_serve; +mod jwt; mod text; pub use b64::{process_decode, process_encode}; pub use csv_convert::process_csv; pub use gen_pass::process_genpass; pub use http_serve::process_http_serve; +pub use jwt::*; pub use text::{process_text_key_generate, process_text_sign, process_text_verify}; diff --git a/src/process/jwt.rs b/src/process/jwt.rs new file mode 100644 index 0000000..0893485 --- /dev/null +++ b/src/process/jwt.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +/// 创建Claims +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + aud: String, + exp: usize, + sub: String, + iat: usize, +} + +/// 签名Jwt +pub fn process_jwt_sign(key: &str, sub: &str, aud: &str, exp: usize) -> Result { + // 创建header 这里暂时用HS256算法 + let header = Header::new(Algorithm::HS256); + // 创建EncodingKey + let encoding_key = EncodingKey::from_secret(key.as_bytes()); + + // issued at: Token 签发时间 + let iat = chrono::Utc::now().timestamp() as usize; + // Token Expires at: Token 过期时间 + let exp = iat + exp; + tracing::info!("exp: {}", exp); + // 创建claims + let claims = Claims { + aud: aud.to_owned(), + exp, + iat, + sub: sub.to_owned(), + }; + + tracing::info!("Claims: {:?}", claims); + + // 生成token + let token = jsonwebtoken::encode(&header, &claims, &encoding_key)?; + + Ok(token) +} + +/// 验证Jwt +pub fn process_jwt_verify(key: &str, token: &str) -> Result { + // 创建DecodingKey + let decoding_key = DecodingKey::from_secret(key.as_bytes()); + + // 创建验证器 + let mut validation = Validation::new(Algorithm::HS256); + // 要设置验证过期时间 但是不验证目标 + validation.validate_aud = false; + validation.validate_exp = true; + + let result = jsonwebtoken::decode::(token, &decoding_key, &validation); + // 这里只要结果是否正常 + Ok(result.is_ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_jwt_sign_verify() -> Result<()> { + let key = "testKey"; + let sub = "testSub"; + let aud = "testAud"; + // 60 秒过期 + let exp_time = 60; + + let token = process_jwt_sign(key, sub, aud, exp_time)?; + + assert!(process_jwt_verify(key, &token)?); + + Ok(()) + } +}