diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1ac9047c07..49c2d43472 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -712,6 +712,7 @@ dependencies = [ "sha2", "sqlx", "srp", + "srp6", "tempdir", "tempfile", "thiserror 2.0.16", @@ -1098,6 +1099,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" + [[package]] name = "hkdf" version = "0.12.4" @@ -1756,6 +1763,7 @@ dependencies = [ "num-integer", "num-traits", "rand 0.8.5", + "serde", ] [[package]] @@ -2886,6 +2894,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "srp6" +version = "1.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279b6afb40ed1f544789f35d689a0b5fadb2f404de70102cdd8918fdce25e47f" +dependencies = [ + "hex-literal", + "num-bigint", + "num-traits", + "rand 0.9.2", + "serde", + "sha2", + "thiserror 2.0.16", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 008587caff..af497cf24e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -29,6 +29,7 @@ zeroize = { version = "1.8", features = ["derive"] } # SRP authentication srp = "0.6" +srp6 = "1.0.0-beta.1" num-bigint = { version = "0.4", features = ["rand"] } sha2 = "0.10" urlencoding = "2.1" diff --git a/rust/src/api/auth.rs b/rust/src/api/auth.rs index 5d18de8e51..191bcd6a82 100644 --- a/rust/src/api/auth.rs +++ b/rust/src/api/auth.rs @@ -6,119 +6,9 @@ use crate::api::models::{ use crate::crypto::{derive_argon_key, derive_login_key}; use crate::models::error::Result; use base64::{Engine, engine::general_purpose::STANDARD}; -use num_bigint::{BigUint, RandBigInt}; -use sha2::{Digest, Sha256}; - -/// Simple SRP-6a client implementation matching Go's behavior -struct SimpleSrpClient { - n: BigUint, // Safe prime - g: BigUint, // Generator -} - -impl SimpleSrpClient { - fn new() -> Self { - // SRP-4096 group parameters (same as used in Go client) - let n_hex = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF"; - let g_hex = "02"; - - Self { - n: BigUint::parse_bytes(n_hex.as_bytes(), 16).unwrap(), - g: BigUint::parse_bytes(g_hex.as_bytes(), 16).unwrap(), - } - } - - fn generate_keys(&self) -> (BigUint, Vec) { - let mut rng = rand::thread_rng(); - let client_secret = rng.gen_biguint_below(&self.n); - let client_public = self.g.modpow(&client_secret, &self.n); - let client_public_bytes = pad_to_n_bytes(client_public.to_bytes_be(), 512); // 4096 bits = 512 bytes - (client_secret, client_public_bytes) - } - - fn compute_proof( - &self, - identity: &[u8], - password: &[u8], - salt: &[u8], - client_secret: &BigUint, - client_public: &[u8], - server_public: &[u8], - ) -> crate::models::error::Result> { - // H(N) XOR H(g) - let h_n = Sha256::digest(&self.n.to_bytes_be()); - let h_g = Sha256::digest(&self.g.to_bytes_be()); - let h_xor: Vec = h_n.iter().zip(h_g.iter()).map(|(a, b)| a ^ b).collect(); - - // H(identity) - let h_identity = Sha256::digest(identity); - - // Compute shared key - let k = compute_k(&self.n, &self.g); - let u = compute_u(client_public, server_public); - let x = compute_x(identity, password, salt); - - let server_b = BigUint::from_bytes_be(server_public); - let v = self.g.modpow(&x, &self.n); - let kv = (&k * &v) % &self.n; - let diff = if server_b >= kv { - &server_b - &kv - } else { - &self.n - (&kv - &server_b) - }; - let ux = &u * &x; - let aux = client_secret + &ux; - let session_key = diff.modpow(&aux, &self.n); - let session_key_hash = Sha256::digest(&session_key.to_bytes_be()); - - // M1 = H(H(N) XOR H(g) | H(identity) | salt | A | B | session_key) - let mut hasher = Sha256::new(); - hasher.update(&h_xor); - hasher.update(&h_identity); - hasher.update(salt); - hasher.update(client_public); - hasher.update(server_public); - hasher.update(&session_key_hash); - - Ok(hasher.finalize().to_vec()) - } -} - -fn pad_to_n_bytes(bytes: Vec, n: usize) -> Vec { - if bytes.len() >= n { - bytes - } else { - let mut padded = vec![0u8; n - bytes.len()]; - padded.extend(bytes); - padded - } -} - -fn compute_k(n: &BigUint, g: &BigUint) -> BigUint { - let mut hasher = Sha256::new(); - hasher.update(&n.to_bytes_be()); - hasher.update(&pad_to_n_bytes(g.to_bytes_be(), 512)); - BigUint::from_bytes_be(&hasher.finalize()) -} - -fn compute_u(client_public: &[u8], server_public: &[u8]) -> BigUint { - let mut hasher = Sha256::new(); - hasher.update(client_public); - hasher.update(server_public); - BigUint::from_bytes_be(&hasher.finalize()) -} - -fn compute_x(identity: &[u8], password: &[u8], salt: &[u8]) -> BigUint { - let mut hasher = Sha256::new(); - hasher.update(identity); - hasher.update(b":"); - hasher.update(password); - let h1 = hasher.finalize(); - - let mut hasher = Sha256::new(); - hasher.update(salt); - hasher.update(&h1); - BigUint::from_bytes_be(&hasher.finalize()) -} +use sha2::Sha256; +use srp::client::SrpClient; +use srp::groups::G_4096; use uuid::Uuid; /// SRP authentication implementation for Ente API @@ -167,6 +57,8 @@ impl<'a> AuthClient<'a> { srp_m1: STANDARD.encode(client_proof), }; + log::debug!("Sending verify-session request: {:?}", request); + self.api .post("/users/srp/verify-session", &request, None) .await @@ -178,6 +70,8 @@ impl<'a> AuthClient<'a> { email: &str, password: &str, ) -> Result<(AuthResponse, Vec)> { + use rand::RngCore; + // Step 1: Get SRP attributes let srp_attrs = self.get_srp_attributes(email).await?; @@ -188,43 +82,68 @@ impl<'a> AuthClient<'a> { srp_attrs.mem_limit as u32, srp_attrs.ops_limit as u32, )?; + log::debug!("KEK (hex): {}", hex::encode(&key_enc_key)); // Step 3: Derive login key let login_key = derive_login_key(&key_enc_key)?; + log::debug!("loginSubKey (base64): {}", STANDARD.encode(&login_key)); + log::debug!("loginSubKey (hex): {}", hex::encode(&login_key)); - // Step 4: Initialize SRP client using the same approach as Go + // Step 4: Initialize SRP client let srp_salt = STANDARD.decode(&srp_attrs.srp_salt)?; - let identity = srp_attrs.srp_user_id.to_string(); + // Use the UUID string directly as bytes (matching TypeScript's Buffer.from(srpUserID)) + let identity = srp_attrs.srp_user_id.to_string().into_bytes(); - // Create SRP client (matching Go's srp.NewClient) - let srp_client = SimpleSrpClient::new(); - let (client_secret, client_public) = srp_client.generate_keys(); + // Create SRP client with 4096-bit group (matching Go's srp.GetParams(4096)) + let client = SrpClient::::new(&G_4096); + + // Generate random ephemeral private key + let mut a = vec![0u8; 64]; + rand::thread_rng().fill_bytes(&mut a); + + // Compute public ephemeral + let a_pub = client.compute_public_ephemeral(&a); // Step 5: Create SRP session + log::debug!("Creating SRP session..."); let session = self - .create_srp_session(&srp_attrs.srp_user_id, &client_public) + .create_srp_session(&srp_attrs.srp_user_id, &a_pub) .await?; + log::debug!("Session created successfully: {}", session.session_id); + + // Add a small delay to avoid potential rate limiting + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; // Step 6: Process server's public key and generate proof let server_public = STANDARD.decode(&session.srp_b)?; - let client_proof = srp_client.compute_proof( - identity.as_bytes(), - &login_key, - &srp_salt, - &client_secret, - &client_public, - &server_public, - )?; + + // Process the server's response and generate client proof + // The srp crate expects: a, username, password, salt, b_pub + // But Ente uses the login_key (derived from password) as the password for SRP + let verifier = client + .process_reply(&a, &identity, &login_key, &srp_salt, &server_public) + .map_err(|e| { + crate::models::error::Error::AuthenticationFailed(format!( + "SRP client process failed: {:?}", + e + )) + })?; // Step 7: Verify session with proof + let proof = verifier.proof(); + let auth_response = self - .verify_srp_session(&srp_attrs.srp_user_id, &session.session_id, &client_proof) + .verify_srp_session(&srp_attrs.srp_user_id, &session.session_id, &proof) .await?; // TODO: Verify server proof if provided // if let Some(srp_m2) = &auth_response.srp_m2 { // let server_proof = STANDARD.decode(srp_m2)?; - // // Verify server proof + // verifier.verify_server(&server_proof).map_err(|_| { + // crate::models::error::Error::AuthenticationFailed( + // "Server proof verification failed".to_string() + // ) + // })?; // } Ok((auth_response, key_enc_key)) diff --git a/rust/src/api/client.rs b/rust/src/api/client.rs index cf0ddd5c3a..228a21d4ee 100644 --- a/rust/src/api/client.rs +++ b/rust/src/api/client.rs @@ -156,11 +156,27 @@ impl ApiClient { let response = self.execute_with_retry(request).await?; if !response.status().is_success() { + let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); - return Err(Error::Generic(format!("API error: {}", error_text))); + + // Try to parse as JSON to get error details + if let Ok(error_json) = serde_json::from_str::(&error_text) { + log::error!( + "API error: status={}, body={}", + status, + serde_json::to_string_pretty(&error_json).unwrap_or(error_text.clone()) + ); + } else { + log::error!("API error: status={}, body={}", status, error_text); + } + + return Err(Error::Generic(format!( + "API error ({}): {}", + status, error_text + ))); } Ok(response.json().await?) @@ -173,17 +189,43 @@ impl ApiClient { B: Serialize, { let url = format!("{}{}", self.base_url, path); + + // Debug log the JSON being sent + if path.contains("verify-session") { + log::debug!( + "POST {} with JSON: {}", + url, + serde_json::to_string_pretty(body)? + ); + } + let request = self.client.post(&url).json(body); let request = self.build_request(request, account_id); let response = self.execute_with_retry(request).await?; if !response.status().is_success() { + let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); - return Err(Error::Generic(format!("API error: {}", error_text))); + + // Try to parse as JSON to get error details + if let Ok(error_json) = serde_json::from_str::(&error_text) { + log::error!( + "API error: status={}, body={}", + status, + serde_json::to_string_pretty(&error_json).unwrap_or(error_text.clone()) + ); + } else { + log::error!("API error: status={}, body={}", status, error_text); + } + + return Err(Error::Generic(format!( + "API error ({}): {}", + status, error_text + ))); } Ok(response.json().await?) @@ -202,11 +244,27 @@ impl ApiClient { let response = self.execute_with_retry(request).await?; if !response.status().is_success() { + let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); - return Err(Error::Generic(format!("API error: {}", error_text))); + + // Try to parse as JSON to get error details + if let Ok(error_json) = serde_json::from_str::(&error_text) { + log::error!( + "API error: status={}, body={}", + status, + serde_json::to_string_pretty(&error_json).unwrap_or(error_text.clone()) + ); + } else { + log::error!("API error: status={}, body={}", status, error_text); + } + + return Err(Error::Generic(format!( + "API error ({}): {}", + status, error_text + ))); } Ok(response.json().await?) @@ -221,11 +279,27 @@ impl ApiClient { let response = self.execute_with_retry(request).await?; if !response.status().is_success() { + let status = response.status(); let error_text = response .text() .await .unwrap_or_else(|_| "Unknown error".to_string()); - return Err(Error::Generic(format!("API error: {}", error_text))); + + // Try to parse as JSON to get error details + if let Ok(error_json) = serde_json::from_str::(&error_text) { + log::error!( + "API error: status={}, body={}", + status, + serde_json::to_string_pretty(&error_json).unwrap_or(error_text.clone()) + ); + } else { + log::error!("API error: status={}, body={}", status, error_text); + } + + return Err(Error::Generic(format!( + "API error ({}): {}", + status, error_text + ))); } Ok(()) diff --git a/rust/src/api/models.rs b/rust/src/api/models.rs index a546a29ecd..7986ce7609 100644 --- a/rust/src/api/models.rs +++ b/rust/src/api/models.rs @@ -4,13 +4,18 @@ use uuid::Uuid; // ========== Authentication Models ========== #[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] pub struct SrpAttributes { + #[serde(rename = "srpUserID")] pub srp_user_id: Uuid, + #[serde(rename = "srpSalt")] pub srp_salt: String, + #[serde(rename = "memLimit")] pub mem_limit: i32, + #[serde(rename = "opsLimit")] pub ops_limit: i32, + #[serde(rename = "kekSalt")] pub kek_salt: String, + #[serde(rename = "isEmailMFAEnabled")] pub is_email_mfa_enabled: bool, } @@ -21,24 +26,28 @@ pub struct GetSrpAttributesResponse { } #[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] pub struct CreateSrpSessionRequest { + #[serde(rename = "srpUserID")] pub srp_user_id: String, + #[serde(rename = "srpA")] pub srp_a: String, } #[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct CreateSrpSessionResponse { + #[serde(rename = "sessionID")] pub session_id: Uuid, + #[serde(rename = "srpB")] pub srp_b: String, } #[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] pub struct VerifySrpSessionRequest { + #[serde(rename = "srpUserID")] pub srp_user_id: String, + #[serde(rename = "sessionID")] pub session_id: String, + #[serde(rename = "srpM1")] pub srp_m1: String, } diff --git a/rust/src/cli/account.rs b/rust/src/cli/account.rs index b15cddbcd0..91f88f3461 100644 --- a/rust/src/cli/account.rs +++ b/rust/src/cli/account.rs @@ -12,7 +12,23 @@ pub enum AccountSubcommands { List, /// Login into existing account - Add, + Add { + /// Email address (optional - will prompt if not provided) + #[arg(long)] + email: Option, + + /// Password (optional - will prompt if not provided) + #[arg(long)] + password: Option, + + /// Specify the app (photos, locker, auth) + #[arg(long, default_value = "photos")] + app: String, + + /// Export directory path + #[arg(long)] + export_dir: Option, + }, /// Update an existing account's export directory Update { diff --git a/rust/src/commands/account.rs b/rust/src/commands/account.rs index 132f35b7dd..f8e82ba550 100644 --- a/rust/src/commands/account.rs +++ b/rust/src/commands/account.rs @@ -14,7 +14,12 @@ use std::path::PathBuf; pub async fn handle_account_command(cmd: AccountCommand, storage: &Storage) -> Result<()> { match cmd.command { AccountSubcommands::List => list_accounts(storage).await, - AccountSubcommands::Add => add_account(storage).await, + AccountSubcommands::Add { + email, + password, + app, + export_dir, + } => add_account(storage, email, password, app, export_dir).await, AccountSubcommands::Update { email, dir, app } => { update_account(storage, &email, &dir, &app).await } @@ -46,29 +51,52 @@ async fn list_accounts(storage: &Storage) -> Result<()> { Ok(()) } -async fn add_account(storage: &Storage) -> Result<()> { +async fn add_account( + storage: &Storage, + email_arg: Option, + password_arg: Option, + app_arg: String, + export_dir_arg: Option, +) -> Result<()> { println!("\n=== Add Ente Account ===\n"); - // Get email - let email: String = Input::new() - .with_prompt("Enter your email address") - .interact_text() - .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; + // Get email (from arg or prompt) + let email = if let Some(email) = email_arg { + email + } else { + Input::new() + .with_prompt("Enter your email address") + .interact_text() + .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))? + }; - // Get app type - let apps = vec!["photos", "locker", "auth"]; - let app_index = Select::new() - .with_prompt("Select the Ente app") - .items(&apps) - .default(0) - .interact() - .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; - - let app = match apps[app_index] { + // Parse app type + let app = match app_arg.to_lowercase().as_str() { "photos" => App::Photos, "locker" => App::Locker, "auth" => App::Auth, - _ => unreachable!(), + _ => { + // If invalid app provided via CLI, use interactive selection + if password_arg.is_some() { + return Err(crate::models::error::Error::InvalidInput(format!( + "Invalid app: {}. Must be one of: photos, locker, auth", + app_arg + ))); + } + let apps = vec!["photos", "locker", "auth"]; + let app_index = Select::new() + .with_prompt("Select the Ente app") + .items(&apps) + .default(0) + .interact() + .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; + match apps[app_index] { + "photos" => App::Photos, + "locker" => App::Locker, + "auth" => App::Auth, + _ => unreachable!(), + } + } }; // Check if account already exists @@ -80,18 +108,26 @@ async fn add_account(storage: &Storage) -> Result<()> { return Ok(()); } - // Get password - let password = Password::new() - .with_prompt("Enter your password") - .interact() - .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; + // Get password (from arg or prompt) + let password = if let Some(password) = password_arg { + password + } else { + Password::new() + .with_prompt("Enter your password") + .interact() + .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))? + }; - // Get export directory - let export_dir: String = Input::new() - .with_prompt("Enter export directory path") - .default(format!("./exports/{}", email)) - .interact_text() - .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; + // Get export directory (from arg or prompt) + let export_dir = if let Some(dir) = export_dir_arg { + dir + } else { + Input::new() + .with_prompt("Enter export directory path") + .default(format!("./exports/{}", email)) + .interact_text() + .map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))? + }; // Validate export directory let export_path = PathBuf::from(&export_dir); @@ -100,8 +136,12 @@ async fn add_account(storage: &Storage) -> Result<()> { std::fs::create_dir_all(&export_path).map_err(|e| crate::models::error::Error::Io(e))?; } - // Initialize API client - let api_client = ApiClient::new(None)?; + // Initialize API client (use ENTE_ENDPOINT env var if set, otherwise default to production) + let api_endpoint = std::env::var("ENTE_ENDPOINT").ok(); + if let Some(ref endpoint) = api_endpoint { + log::debug!("Using custom API endpoint: {}", endpoint); + } + let api_client = ApiClient::new(api_endpoint)?; let auth_client = AuthClient::new(&api_client); println!("\nAuthenticating with Ente servers..."); diff --git a/rust/src/crypto/argon.rs b/rust/src/crypto/argon.rs index f0acfa371c..0acdeebbfe 100644 --- a/rust/src/crypto/argon.rs +++ b/rust/src/crypto/argon.rs @@ -27,17 +27,20 @@ pub fn derive_argon_key( ))); } - let mut key = vec![0u8; 32]; // 32 bytes output + let mut key = vec![0u8; sodium::crypto_secretbox_KEYBYTES as usize]; // 32 bytes output + + // Convert password to bytes (matching JS sodium.from_string) + let password_bytes = password.as_bytes(); let result = unsafe { sodium::crypto_pwhash( key.as_mut_ptr(), key.len() as u64, - password.as_ptr() as *const std::ffi::c_char, - password.len() as u64, + password_bytes.as_ptr() as *const std::ffi::c_char, + password_bytes.len() as u64, salt_bytes.as_ptr(), ops_limit as u64, - mem_limit as usize * 1024, // Convert from KB to bytes + mem_limit as usize, // API sends bytes, libsodium-sys expects bytes sodium::crypto_pwhash_ALG_ARGON2ID13 as i32, ) }; diff --git a/rust/src/crypto/kdf.rs b/rust/src/crypto/kdf.rs index 135e6e1261..52fccd72af 100644 --- a/rust/src/crypto/kdf.rs +++ b/rust/src/crypto/kdf.rs @@ -6,61 +6,30 @@ const LOGIN_SUB_KEY_ID: u64 = 1; const LOGIN_SUB_KEY_CONTEXT: &[u8] = b"loginctx"; /// Derive login key from key encryption key -/// This matches the Go implementation's DeriveLoginKey function +/// This matches the web implementation's deriveSRPLoginSubKey function pub fn derive_login_key(key_enc_key: &[u8]) -> Result> { - let sub_key = derive_sub_key( - key_enc_key, - LOGIN_SUB_KEY_CONTEXT, - LOGIN_SUB_KEY_ID, - LOGIN_SUB_KEY_LEN, - )?; + // Derive 32 bytes using crypto_kdf_derive_from_key + let mut sub_key = vec![0u8; LOGIN_SUB_KEY_LEN]; - // Return the first 16 bytes of the derived key - Ok(sub_key[..16].to_vec()) -} - -/// Derive a subkey using Blake2b (matching Go's deriveSubKey) -fn derive_sub_key( - master_key: &[u8], - context: &[u8], - sub_key_id: u64, - sub_key_length: usize, -) -> Result> { - const CRYPTO_KDF_BLAKE2B_BYTES_MIN: usize = 16; - const CRYPTO_KDF_BLAKE2B_BYTES_MAX: usize = 64; - - if !(CRYPTO_KDF_BLAKE2B_BYTES_MIN..=CRYPTO_KDF_BLAKE2B_BYTES_MAX).contains(&sub_key_length) { - return Err(Error::Crypto("subKeyLength out of bounds".into())); - } - - // Pad the context to 16 bytes (PERSONALBYTES) - let mut ctx_padded = vec![0u8; sodium::crypto_generichash_blake2b_PERSONALBYTES as usize]; - let context_len = context.len().min(ctx_padded.len()); - ctx_padded[..context_len].copy_from_slice(&context[..context_len]); - - // Convert subKeyID to byte slice and pad to 16 bytes (SALTBYTES) - let mut salt = vec![0u8; sodium::crypto_generichash_blake2b_SALTBYTES as usize]; - salt[..8].copy_from_slice(&sub_key_id.to_le_bytes()); - - // Create output buffer - let mut out = vec![0u8; sub_key_length]; + // Ensure context is exactly 8 bytes (crypto_kdf_CONTEXTBYTES) + let mut context = [0u8; sodium::crypto_kdf_CONTEXTBYTES as usize]; + let context_len = LOGIN_SUB_KEY_CONTEXT.len().min(context.len()); + context[..context_len].copy_from_slice(&LOGIN_SUB_KEY_CONTEXT[..context_len]); let result = unsafe { - sodium::crypto_generichash_blake2b_salt_personal( - out.as_mut_ptr(), - sub_key_length, - std::ptr::null(), // No input data, just using key, salt, and personalization - 0, - master_key.as_ptr(), - master_key.len(), - salt.as_ptr(), - ctx_padded.as_ptr(), + sodium::crypto_kdf_derive_from_key( + sub_key.as_mut_ptr(), + LOGIN_SUB_KEY_LEN, + LOGIN_SUB_KEY_ID, + context.as_ptr(), + key_enc_key.as_ptr(), ) }; if result != 0 { - return Err(Error::Crypto("Failed to derive subkey with Blake2b".into())); + return Err(Error::Crypto("Failed to derive login subkey".into())); } - Ok(out) + // Return the first 16 bytes of the derived key (matching web implementation) + Ok(sub_key[..16].to_vec()) }