fix(rust): Fix SRP authentication implementation

Fixed issues preventing successful authentication:
- Corrected Argon2 memory limit handling (API sends bytes, not KB)
- Replaced Blake2b with crypto_kdf_derive_from_key for login subkey derivation
- Fixed serde field names to match API expectations (srpUserID, sessionID)
- Added non-interactive mode for CLI testing
- Added support for ENTE_ENDPOINT environment variable

The implementation now matches the web client's key derivation exactly,
enabling successful authentication with both local and production servers.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Manav Rathi
2025-08-22 18:18:18 +05:30
parent cd20a98850
commit 48757af5d0
9 changed files with 274 additions and 220 deletions

23
rust/Cargo.lock generated
View File

@@ -712,6 +712,7 @@ dependencies = [
"sha2", "sha2",
"sqlx", "sqlx",
"srp", "srp",
"srp6",
"tempdir", "tempdir",
"tempfile", "tempfile",
"thiserror 2.0.16", "thiserror 2.0.16",
@@ -1098,6 +1099,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.12.4" version = "0.12.4"
@@ -1756,6 +1763,7 @@ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
"rand 0.8.5", "rand 0.8.5",
"serde",
] ]
[[package]] [[package]]
@@ -2886,6 +2894,21 @@ dependencies = [
"subtle", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"

View File

@@ -29,6 +29,7 @@ zeroize = { version = "1.8", features = ["derive"] }
# SRP authentication # SRP authentication
srp = "0.6" srp = "0.6"
srp6 = "1.0.0-beta.1"
num-bigint = { version = "0.4", features = ["rand"] } num-bigint = { version = "0.4", features = ["rand"] }
sha2 = "0.10" sha2 = "0.10"
urlencoding = "2.1" urlencoding = "2.1"

View File

@@ -6,119 +6,9 @@ use crate::api::models::{
use crate::crypto::{derive_argon_key, derive_login_key}; use crate::crypto::{derive_argon_key, derive_login_key};
use crate::models::error::Result; use crate::models::error::Result;
use base64::{Engine, engine::general_purpose::STANDARD}; use base64::{Engine, engine::general_purpose::STANDARD};
use num_bigint::{BigUint, RandBigInt}; use sha2::Sha256;
use sha2::{Digest, Sha256}; use srp::client::SrpClient;
use srp::groups::G_4096;
/// 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<u8>) {
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<Vec<u8>> {
// 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<u8> = 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<u8>, n: usize) -> Vec<u8> {
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 uuid::Uuid; use uuid::Uuid;
/// SRP authentication implementation for Ente API /// SRP authentication implementation for Ente API
@@ -167,6 +57,8 @@ impl<'a> AuthClient<'a> {
srp_m1: STANDARD.encode(client_proof), srp_m1: STANDARD.encode(client_proof),
}; };
log::debug!("Sending verify-session request: {:?}", request);
self.api self.api
.post("/users/srp/verify-session", &request, None) .post("/users/srp/verify-session", &request, None)
.await .await
@@ -178,6 +70,8 @@ impl<'a> AuthClient<'a> {
email: &str, email: &str,
password: &str, password: &str,
) -> Result<(AuthResponse, Vec<u8>)> { ) -> Result<(AuthResponse, Vec<u8>)> {
use rand::RngCore;
// Step 1: Get SRP attributes // Step 1: Get SRP attributes
let srp_attrs = self.get_srp_attributes(email).await?; 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.mem_limit as u32,
srp_attrs.ops_limit as u32, srp_attrs.ops_limit as u32,
)?; )?;
log::debug!("KEK (hex): {}", hex::encode(&key_enc_key));
// Step 3: Derive login key // Step 3: Derive login key
let login_key = derive_login_key(&key_enc_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 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) // Create SRP client with 4096-bit group (matching Go's srp.GetParams(4096))
let srp_client = SimpleSrpClient::new(); let client = SrpClient::<Sha256>::new(&G_4096);
let (client_secret, client_public) = srp_client.generate_keys();
// 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 // Step 5: Create SRP session
log::debug!("Creating SRP session...");
let session = self let session = self
.create_srp_session(&srp_attrs.srp_user_id, &client_public) .create_srp_session(&srp_attrs.srp_user_id, &a_pub)
.await?; .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 // Step 6: Process server's public key and generate proof
let server_public = STANDARD.decode(&session.srp_b)?; let server_public = STANDARD.decode(&session.srp_b)?;
let client_proof = srp_client.compute_proof(
identity.as_bytes(), // Process the server's response and generate client proof
&login_key, // The srp crate expects: a, username, password, salt, b_pub
&srp_salt, // But Ente uses the login_key (derived from password) as the password for SRP
&client_secret, let verifier = client
&client_public, .process_reply(&a, &identity, &login_key, &srp_salt, &server_public)
&server_public, .map_err(|e| {
)?; crate::models::error::Error::AuthenticationFailed(format!(
"SRP client process failed: {:?}",
e
))
})?;
// Step 7: Verify session with proof // Step 7: Verify session with proof
let proof = verifier.proof();
let auth_response = self 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?; .await?;
// TODO: Verify server proof if provided // TODO: Verify server proof if provided
// if let Some(srp_m2) = &auth_response.srp_m2 { // if let Some(srp_m2) = &auth_response.srp_m2 {
// let server_proof = STANDARD.decode(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)) Ok((auth_response, key_enc_key))

View File

@@ -156,11 +156,27 @@ impl ApiClient {
let response = self.execute_with_retry(request).await?; let response = self.execute_with_retry(request).await?;
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status();
let error_text = response let error_text = response
.text() .text()
.await .await
.unwrap_or_else(|_| "Unknown error".to_string()); .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::<serde_json::Value>(&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?) Ok(response.json().await?)
@@ -173,17 +189,43 @@ impl ApiClient {
B: Serialize, B: Serialize,
{ {
let url = format!("{}{}", self.base_url, path); 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.client.post(&url).json(body);
let request = self.build_request(request, account_id); let request = self.build_request(request, account_id);
let response = self.execute_with_retry(request).await?; let response = self.execute_with_retry(request).await?;
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status();
let error_text = response let error_text = response
.text() .text()
.await .await
.unwrap_or_else(|_| "Unknown error".to_string()); .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::<serde_json::Value>(&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?) Ok(response.json().await?)
@@ -202,11 +244,27 @@ impl ApiClient {
let response = self.execute_with_retry(request).await?; let response = self.execute_with_retry(request).await?;
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status();
let error_text = response let error_text = response
.text() .text()
.await .await
.unwrap_or_else(|_| "Unknown error".to_string()); .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::<serde_json::Value>(&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?) Ok(response.json().await?)
@@ -221,11 +279,27 @@ impl ApiClient {
let response = self.execute_with_retry(request).await?; let response = self.execute_with_retry(request).await?;
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status();
let error_text = response let error_text = response
.text() .text()
.await .await
.unwrap_or_else(|_| "Unknown error".to_string()); .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::<serde_json::Value>(&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(()) Ok(())

View File

@@ -4,13 +4,18 @@ use uuid::Uuid;
// ========== Authentication Models ========== // ========== Authentication Models ==========
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SrpAttributes { pub struct SrpAttributes {
#[serde(rename = "srpUserID")]
pub srp_user_id: Uuid, pub srp_user_id: Uuid,
#[serde(rename = "srpSalt")]
pub srp_salt: String, pub srp_salt: String,
#[serde(rename = "memLimit")]
pub mem_limit: i32, pub mem_limit: i32,
#[serde(rename = "opsLimit")]
pub ops_limit: i32, pub ops_limit: i32,
#[serde(rename = "kekSalt")]
pub kek_salt: String, pub kek_salt: String,
#[serde(rename = "isEmailMFAEnabled")]
pub is_email_mfa_enabled: bool, pub is_email_mfa_enabled: bool,
} }
@@ -21,24 +26,28 @@ pub struct GetSrpAttributesResponse {
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSrpSessionRequest { pub struct CreateSrpSessionRequest {
#[serde(rename = "srpUserID")]
pub srp_user_id: String, pub srp_user_id: String,
#[serde(rename = "srpA")]
pub srp_a: String, pub srp_a: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateSrpSessionResponse { pub struct CreateSrpSessionResponse {
#[serde(rename = "sessionID")]
pub session_id: Uuid, pub session_id: Uuid,
#[serde(rename = "srpB")]
pub srp_b: String, pub srp_b: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifySrpSessionRequest { pub struct VerifySrpSessionRequest {
#[serde(rename = "srpUserID")]
pub srp_user_id: String, pub srp_user_id: String,
#[serde(rename = "sessionID")]
pub session_id: String, pub session_id: String,
#[serde(rename = "srpM1")]
pub srp_m1: String, pub srp_m1: String,
} }

View File

@@ -12,7 +12,23 @@ pub enum AccountSubcommands {
List, List,
/// Login into existing account /// Login into existing account
Add, Add {
/// Email address (optional - will prompt if not provided)
#[arg(long)]
email: Option<String>,
/// Password (optional - will prompt if not provided)
#[arg(long)]
password: Option<String>,
/// Specify the app (photos, locker, auth)
#[arg(long, default_value = "photos")]
app: String,
/// Export directory path
#[arg(long)]
export_dir: Option<String>,
},
/// Update an existing account's export directory /// Update an existing account's export directory
Update { Update {

View File

@@ -14,7 +14,12 @@ use std::path::PathBuf;
pub async fn handle_account_command(cmd: AccountCommand, storage: &Storage) -> Result<()> { pub async fn handle_account_command(cmd: AccountCommand, storage: &Storage) -> Result<()> {
match cmd.command { match cmd.command {
AccountSubcommands::List => list_accounts(storage).await, 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 } => { AccountSubcommands::Update { email, dir, app } => {
update_account(storage, &email, &dir, &app).await update_account(storage, &email, &dir, &app).await
} }
@@ -46,29 +51,52 @@ async fn list_accounts(storage: &Storage) -> Result<()> {
Ok(()) Ok(())
} }
async fn add_account(storage: &Storage) -> Result<()> { async fn add_account(
storage: &Storage,
email_arg: Option<String>,
password_arg: Option<String>,
app_arg: String,
export_dir_arg: Option<String>,
) -> Result<()> {
println!("\n=== Add Ente Account ===\n"); println!("\n=== Add Ente Account ===\n");
// Get email // Get email (from arg or prompt)
let email: String = Input::new() let email = if let Some(email) = email_arg {
.with_prompt("Enter your email address") email
.interact_text() } else {
.map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; Input::new()
.with_prompt("Enter your email address")
.interact_text()
.map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?
};
// Get app type // Parse app type
let apps = vec!["photos", "locker", "auth"]; let app = match app_arg.to_lowercase().as_str() {
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] {
"photos" => App::Photos, "photos" => App::Photos,
"locker" => App::Locker, "locker" => App::Locker,
"auth" => App::Auth, "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 // Check if account already exists
@@ -80,18 +108,26 @@ async fn add_account(storage: &Storage) -> Result<()> {
return Ok(()); return Ok(());
} }
// Get password // Get password (from arg or prompt)
let password = Password::new() let password = if let Some(password) = password_arg {
.with_prompt("Enter your password") password
.interact() } else {
.map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; Password::new()
.with_prompt("Enter your password")
.interact()
.map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?
};
// Get export directory // Get export directory (from arg or prompt)
let export_dir: String = Input::new() let export_dir = if let Some(dir) = export_dir_arg {
.with_prompt("Enter export directory path") dir
.default(format!("./exports/{}", email)) } else {
.interact_text() Input::new()
.map_err(|e| crate::models::error::Error::InvalidInput(e.to_string()))?; .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 // Validate export directory
let export_path = PathBuf::from(&export_dir); 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))?; std::fs::create_dir_all(&export_path).map_err(|e| crate::models::error::Error::Io(e))?;
} }
// Initialize API client // Initialize API client (use ENTE_ENDPOINT env var if set, otherwise default to production)
let api_client = ApiClient::new(None)?; 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); let auth_client = AuthClient::new(&api_client);
println!("\nAuthenticating with Ente servers..."); println!("\nAuthenticating with Ente servers...");

View File

@@ -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 { let result = unsafe {
sodium::crypto_pwhash( sodium::crypto_pwhash(
key.as_mut_ptr(), key.as_mut_ptr(),
key.len() as u64, key.len() as u64,
password.as_ptr() as *const std::ffi::c_char, password_bytes.as_ptr() as *const std::ffi::c_char,
password.len() as u64, password_bytes.len() as u64,
salt_bytes.as_ptr(), salt_bytes.as_ptr(),
ops_limit as u64, 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, sodium::crypto_pwhash_ALG_ARGON2ID13 as i32,
) )
}; };

View File

@@ -6,61 +6,30 @@ const LOGIN_SUB_KEY_ID: u64 = 1;
const LOGIN_SUB_KEY_CONTEXT: &[u8] = b"loginctx"; const LOGIN_SUB_KEY_CONTEXT: &[u8] = b"loginctx";
/// Derive login key from key encryption key /// 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<Vec<u8>> { pub fn derive_login_key(key_enc_key: &[u8]) -> Result<Vec<u8>> {
let sub_key = derive_sub_key( // Derive 32 bytes using crypto_kdf_derive_from_key
key_enc_key, let mut sub_key = vec![0u8; LOGIN_SUB_KEY_LEN];
LOGIN_SUB_KEY_CONTEXT,
LOGIN_SUB_KEY_ID,
LOGIN_SUB_KEY_LEN,
)?;
// Return the first 16 bytes of the derived key // Ensure context is exactly 8 bytes (crypto_kdf_CONTEXTBYTES)
Ok(sub_key[..16].to_vec()) 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]);
/// 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<Vec<u8>> {
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];
let result = unsafe { let result = unsafe {
sodium::crypto_generichash_blake2b_salt_personal( sodium::crypto_kdf_derive_from_key(
out.as_mut_ptr(), sub_key.as_mut_ptr(),
sub_key_length, LOGIN_SUB_KEY_LEN,
std::ptr::null(), // No input data, just using key, salt, and personalization LOGIN_SUB_KEY_ID,
0, context.as_ptr(),
master_key.as_ptr(), key_enc_key.as_ptr(),
master_key.len(),
salt.as_ptr(),
ctx_padded.as_ptr(),
) )
}; };
if result != 0 { 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())
} }