diff --git a/Cargo.lock b/Cargo.lock index a10f342d6..d2c5ee8e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,19 @@ dependencies = [ "url", ] +[[package]] +name = "appauth" +version = "0.0.1" +dependencies = [ + "base64-simd", + "common", + "hex-simd", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -1418,6 +1431,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.0" @@ -2314,6 +2333,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.4.1" @@ -2375,6 +2405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.6", "subtle", ] @@ -2386,7 +2417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c478574b20020306f98d61c8ca3322d762e1ff08117422ac6106438605ea516" dependencies = [ "block-buffer 0.11.0-rc.4", - "const-oid", + "const-oid 0.10.0", "crypto-common 0.2.0-rc.2", "subtle", ] @@ -4510,6 +4541,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lazycell" @@ -5149,6 +5183,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -5849,6 +5900,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -6083,6 +6143,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -6919,6 +7000,26 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-embed" version = "8.6.0" @@ -6997,6 +7098,7 @@ name = "rustfs" version = "0.1.0" dependencies = [ "api", + "appauth", "async-trait", "atoi", "axum", @@ -7638,6 +7740,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -7798,6 +7910,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlparser" version = "0.54.0" diff --git a/Cargo.toml b/Cargo.toml index 76edbe035..01c6f98bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "cli/rustfs-gui", # Graphical user interface client "packages/obs", # Observability utilities "s3select/api", - "s3select/query", + "s3select/query", "appauth", ] resolver = "2" diff --git a/appauth/Cargo.toml b/appauth/Cargo.toml new file mode 100644 index 000000000..b638313e1 --- /dev/null +++ b/appauth/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "appauth" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +base64-simd = "0.8.0" +common.workspace = true +hex-simd = "0.8.0" +rand.workspace = true +rsa = "0.9.8" +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/appauth/src/lib.rs b/appauth/src/lib.rs new file mode 100644 index 000000000..79c66ba63 --- /dev/null +++ b/appauth/src/lib.rs @@ -0,0 +1 @@ +pub mod token; diff --git a/appauth/src/token.rs b/appauth/src/token.rs new file mode 100644 index 000000000..6be037dd5 --- /dev/null +++ b/appauth/src/token.rs @@ -0,0 +1,110 @@ +use common::error::Result; +use rsa::Pkcs1v15Encrypt; +use rsa::{ + pkcs8::{DecodePrivateKey, DecodePublicKey}, + RsaPrivateKey, RsaPublicKey, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Token { + pub name: String, // 应用ID + pub expired: u64, // 到期时间 (UNIX时间戳) +} + +// 公钥生成Token +// [token] Token对象 +// [key] 公钥字符串 +// 返回base64处理的加密字符串 +pub fn gencode(token: &Token, key: &str) -> Result { + let data = serde_json::to_vec(token)?; + let public_key = RsaPublicKey::from_public_key_pem(key)?; + let encrypted_data = public_key.encrypt(&mut rand::thread_rng(), Pkcs1v15Encrypt, &data)?; + Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data)) +} + +// 私钥解析Token +// [token] base64处理的加密字符串 +// [key] 私钥字符串 +// 返回Token对象 +pub fn parse(token: &str, key: &str) -> Result { + let encrypted_data = base64_simd::URL_SAFE_NO_PAD.decode_to_vec(token.as_bytes())?; + let private_key = RsaPrivateKey::from_pkcs8_pem(key)?; + let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data)?; + let res: Token = serde_json::from_slice(&decrypted_data)?; + Ok(res) +} + +pub fn parse_license(license: &str) -> Result { + parse(license, TEST_PRIVATE_KEY) + // match parse(license, TEST_PRIVATE_KEY) { + // Ok(token) => { + // if token.expired > SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() { + // Ok(token) + // } else { + // Err("Token expired".into()) + // } + // } + // Err(e) => Err(e), + // } +} + +static TEST_PRIVATE_KEY:&str ="-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj86SrJIuxSxR6\nBJ/dlJEUIj6NeBRnhLQlCDdovuz61+7kJXVcxaR66w4m8W7SLEUP+IlPtnn6vmiG\n7XMhGNHIr7r1JsEVVLhZmL3tKI66DEZl786ZhG81BWqUlmcooIPS8UEPZNqJXLuz\nVGhxNyVGbj/tV7QC2pSISnKaixc+nrhxvo7w56p5qrm9tik0PjTgfZsUePkoBsSN\npoRkAauS14MAzK6HGB75CzG3dZqXUNWSWVocoWtQbZUwFGXyzU01ammsHQDvc2xu\nK1RQpd1qYH5bOWZ0N0aPFwT0r59HztFXg9sbjsnuhO1A7OiUOkc6iGVuJ0wm/9nA\nwZIBqzgjAgMBAAECggEAPMpeSEbotPhNw2BrllE76ec4omPfzPJbiU+em+wPGoNu\nRJHPDnMKJbl6Kd5jZPKdOOrCnxfd6qcnQsBQa/kz7+GYxMV12l7ra+1Cnujm4v0i\nLTHZvPpp8ZLsjeOmpF3AAzsJEJgon74OqtOlVjVIUPEYKvzV9ijt4gsYq0zfdYv0\nhrTMzyrGM4/UvKLsFIBROAfCeWfA7sXLGH8JhrRAyDrtCPzGtyyAmzoHKHtHafcB\nuyPFw/IP8otAgpDk5iiQPNkH0WwzAQIm12oHuNUa66NwUK4WEjXTnDg8KeWLHHNv\nIfN8vdbZchMUpMIvvkr7is315d8f2cHCB5gEO+GWAQKBgQDR/0xNll+FYaiUKCPZ\nvkOCAd3l5mRhsqnjPQ/6Ul1lAyYWpoJSFMrGGn/WKTa/FVFJRTGbBjwP+Mx10bfb\ngUg2GILDTISUh54fp4zngvTi9w4MWGKXrb7I1jPkM3vbJfC/v2fraQ/r7qHPpO2L\nf6ZbGxasIlSvr37KeGoelwcAQQKBgQDH3hmOTS2Hl6D4EXdq5meHKrfeoicGN7m8\noQK7u8iwn1R9zK5nh6IXxBhKYNXNwdCQtBZVRvFjjZ56SZJb7lKqa1BcTsgJfZCy\nnI3Uu4UykrECAH8AVCVqBXUDJmeA2yE+gDAtYEjvhSDHpUfWxoGHr0B/Oqk2Lxc/\npRy1qV5fYwKBgBWSL/hYVf+RhIuTg/s9/BlCr9SJ0g3nGGRrRVTlWQqjRCpXeFOO\nJzYqSq9pFGKUggEQxoOyJEFPwVDo9gXqRcyov+Xn2kaXl7qQr3yoixc1YZALFDWY\nd1ySBEqQr0xXnV9U/gvEgwotPRnjSzNlLWV2ZuHPtPtG/7M0o1H5GZMBAoGAKr3N\nW0gX53o+my4pCnxRQW+aOIsWq1a5aqRIEFudFGBOUkS2Oz+fI1P1GdrRfhnnfzpz\n2DK+plp/vIkFOpGhrf4bBlJ2psjqa7fdANRFLMaAAfyXLDvScHTQTCcnVUAHQPVq\n2BlSH56pnugyj7SNuLV6pnql+wdhAmRN2m9o1h8CgYAbX2juSr4ioXwnYjOUdrIY\n4+ERvHcXdjoJmmPcAm4y5NbSqLXyU0FQmplNMt2A5LlniWVJ9KNdjAQUt60FZw/+\nr76LdxXaHNZghyx0BOs7mtq5unSQXamZ8KixasfhE9uz3ij1jXjG6hafWkS8/68I\nuWbaZqgvy7a9oPHYlKH7Jg==\n-----END PRIVATE KEY-----\n"; + +#[cfg(test)] +mod tests { + use super::*; + use rsa::{ + pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}, + RsaPrivateKey, + }; + use std::time::{SystemTime, UNIX_EPOCH}; + #[test] + fn test_gencode_and_parse() { + let mut rng = rand::thread_rng(); + let bits = 2048; + let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key"); + let public_key = RsaPublicKey::from(&private_key); + + let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap(); + let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + + let token = Token { + name: "test_app".to_string(), + expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now + }; + + let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token"); + + let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token"); + + assert_eq!(token.name, decoded.name); + assert_eq!(token.expired, decoded.expired); + } + + #[test] + fn test_parse_invalid_token() { + let private_key_pem = RsaPrivateKey::new(&mut rand::thread_rng(), 2048) + .expect("Failed to generate private key") + .to_pkcs8_pem(LineEnding::LF) + .unwrap(); + + let invalid_token = "invalid_base64_token"; + let result = parse(invalid_token, &private_key_pem); + + assert!(result.is_err()); + } + + #[test] + fn test_gencode_with_invalid_key() { + let token = Token { + name: "test_app".to_string(), + expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now + }; + + let invalid_key = "invalid_public_key"; + let result = gencode(&token, invalid_key); + + assert!(result.is_err()); + } +} diff --git a/rustfs/Cargo.toml b/rustfs/Cargo.toml index db348d89a..153db6bce 100644 --- a/rustfs/Cargo.toml +++ b/rustfs/Cargo.toml @@ -87,6 +87,7 @@ rust-embed = { workspace = true, features = ["interpolate-folder-path"] } local-ip-address = { workspace = true } chrono = { workspace = true } rustfs-obs = { workspace = true } +appauth = { version = "0.0.1", path = "../appauth" } [build-dependencies] prost-build.workspace = true diff --git a/rustfs/src/config/mod.rs b/rustfs/src/config/mod.rs index f955ccc78..673cffc80 100644 --- a/rustfs/src/config/mod.rs +++ b/rustfs/src/config/mod.rs @@ -2,6 +2,7 @@ use clap::Parser; use const_str::concat; use ecstore::global::DEFAULT_PORT; use std::string::ToString; +use std::sync::OnceLock; shadow_rs::shadow!(build); @@ -100,4 +101,20 @@ pub struct Opt { /// tls path for rustfs api and console. #[arg(long, env = "RUSTFS_TLS_PATH")] pub tls_path: Option, + + #[arg(long, env = "RUSTFS_LICENSE")] + pub license: Option, +} + +lazy_static::lazy_static! { + pub static ref OPT: OnceLock = OnceLock::new(); +} + +pub fn init_config() { + let opt = Opt::parse(); + OPT.set(opt).expect("Failed to set global config"); +} + +pub fn get_config() -> &'static Opt { + OPT.get().expect("Global config not initialized") } diff --git a/rustfs/src/console.rs b/rustfs/src/console.rs index 602997b64..5ed9dc37e 100644 --- a/rustfs/src/console.rs +++ b/rustfs/src/console.rs @@ -1,3 +1,4 @@ +use crate::config; use axum::{ body::Body, extract::Host, @@ -158,6 +159,28 @@ pub(crate) fn init_console_cfg(local_ip: Ipv4Addr, port: u16) { // host.parse::().is_ok() || host.parse::().is_ok() // } +async fn license_handler() -> impl IntoResponse { + let license = config::get_config() + .license + .as_ref() + .map(|license| { + if license.is_empty() { + return None; + } + match appauth::token::parse_license(license) { + Ok(token) => Some(token), + Err(_) => None, + } + }) + .unwrap_or_default(); + + Response::builder() + .header("content-type", "application/json") + .status(StatusCode::OK) + .body(Body::from(serde_json::to_string(&license).unwrap_or_default())) + .unwrap() +} + #[allow(clippy::const_is_empty)] async fn config_handler(Host(host): Host) -> impl IntoResponse { let host_with_port = if host.contains(':') { host } else { format!("{}:80", host) }; @@ -203,6 +226,7 @@ pub async fn start_static_file_server( ) { // Create a route let app = Router::new() + .route("/license", get(license_handler)) .route("/config.json", get(config_handler)) .nest_service("/", get(static_handler)); let local_addr: SocketAddr = addrs.parse().expect("Failed to parse socket address"); diff --git a/rustfs/src/license.rs b/rustfs/src/license.rs new file mode 100644 index 000000000..752c10457 --- /dev/null +++ b/rustfs/src/license.rs @@ -0,0 +1,29 @@ +use crate::config; +use common::error::{Error, Result}; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use tracing::error; +use tracing::info; + +pub fn license_check() -> Result<()> { + let inval_license = config::get_config().license.as_ref().map(|license| { + if license.is_empty() { + error!("License is empty"); + return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string())); + } + let token = appauth::token::parse_license(license)?; + if token.expired < SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() { + error!("License expired"); + return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string())); + } + + info!("License is valid ! expired at {}", token.expired); + Ok(()) + }); + + if inval_license.is_none() || inval_license.is_some_and(|v| v.is_err()) { + return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string())); + } + + Ok(()) +} diff --git a/rustfs/src/main.rs b/rustfs/src/main.rs index 318594cbf..572665d2b 100644 --- a/rustfs/src/main.rs +++ b/rustfs/src/main.rs @@ -3,6 +3,7 @@ mod auth; mod config; mod console; mod grpc; +pub mod license; mod logging; mod service; mod storage; @@ -11,6 +12,7 @@ mod utils; use crate::auth::IAMAuth; use crate::console::{init_console_cfg, CONSOLE_CONFIG}; use crate::utils::error; +// Ensure the correct path for parse_license is imported use chrono::Datelike; use clap::Parser; use common::{ @@ -92,6 +94,8 @@ fn print_server_info() { #[tokio::main] async fn main() -> Result<()> { + config::init_config(); + // Parse the obtained parameters let opt = config::Opt::parse(); diff --git a/rustfs/src/storage/access.rs b/rustfs/src/storage/access.rs index a3a3b0da3..e2cad0c07 100644 --- a/rustfs/src/storage/access.rs +++ b/rustfs/src/storage/access.rs @@ -1,5 +1,6 @@ use super::ecfs::FS; use crate::auth::{check_key_valid, get_condition_values, get_session_token}; +use crate::license::license_check; use ecstore::bucket::policy_sys::PolicySys; use iam::error::Error as IamError; use policy::auth; @@ -180,6 +181,8 @@ impl S3Access for FS { /// /// This method returns `Ok(())` by default. async fn create_bucket(&self, req: &mut S3Request) -> S3Result<()> { + license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; + let req_info = req.extensions.get_mut::().expect("ReqInfo not found"); req_info.bucket = Some(req.input.bucket.clone()); @@ -241,6 +244,7 @@ impl S3Access for FS { /// /// This method returns `Ok(())` by default. async fn create_multipart_upload(&self, _req: &mut S3Request) -> S3Result<()> { + license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; Ok(()) } @@ -995,6 +999,8 @@ impl S3Access for FS { /// /// This method returns `Ok(())` by default. async fn put_object(&self, req: &mut S3Request) -> S3Result<()> { + license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?; + let req_info = req.extensions.get_mut::().expect("ReqInfo not found"); req_info.bucket = Some(req.input.bucket.clone()); req_info.object = Some(req.input.key.clone());