From 0fef9a8fec6e912320108898d08c3e56fe801318 Mon Sep 17 00:00:00 2001 From: expectocode Date: Thu, 13 Oct 2022 00:16:11 +0100 Subject: [PATCH] WIP implement 2fa login --- .../java/dev/lonami/talaria/TalariaApp.kt | 2 +- .../dev/lonami/talaria/bindings/Native.kt | 5 +- .../java/dev/lonami/talaria/ui/LoginScreen.kt | 55 ++++++++++++- app/src/main/res/values/strings.xml | 4 + native/src/lib.rs | 77 ++++++++++++++++--- 5 files changed, 125 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt index 4952b7f..d113930 100644 --- a/app/src/main/java/dev/lonami/talaria/TalariaApp.kt +++ b/app/src/main/java/dev/lonami/talaria/TalariaApp.kt @@ -72,7 +72,7 @@ fun TalariaApp() { ChatScreen(selectedDialog) } composable(route = TalariaScreen.Login.name) { - LoginScreen(onConfirmOtp = { + LoginScreen(onLoginComplete = { navController.navigate(TalariaScreen.Dialog.name) }) } diff --git a/app/src/main/java/dev/lonami/talaria/bindings/Native.kt b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt index 607fdaf..2dd4d02 100644 --- a/app/src/main/java/dev/lonami/talaria/bindings/Native.kt +++ b/app/src/main/java/dev/lonami/talaria/bindings/Native.kt @@ -8,7 +8,10 @@ object Native { external fun initClient() external fun needLogin(): Boolean external fun requestLoginCode(phone: String): Long - external fun signIn(tokenPtr: Long, code: String) + // Returns true if sign in complete, false if 2fa password needed + // TODO: more rich return type including password hint / type of error + external fun signIn(tokenPtr: Long, code: String): Boolean + external fun checkPassword(password: String) external fun getDialogs(): Long external fun dialogCount(dialogsPtr: Long): Int external fun dialogPacked(dialogsPtr: Long, index: Int): String diff --git a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt index 26a1497..09edce0 100644 --- a/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt +++ b/app/src/main/java/dev/lonami/talaria/ui/LoginScreen.kt @@ -24,10 +24,12 @@ import dev.lonami.talaria.ui.theme.TalariaTheme enum class LoginStage { ASK_PHONE, ASK_CODE, + ASK_PASSWORD, } fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty() fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5 +fun isPasswordValid(password: String): Boolean = password.isNotEmpty() @Composable fun PhoneInput(phone: String, onPhoneChanged: (String) -> Unit, onSendCode: () -> Unit) { @@ -80,16 +82,50 @@ fun OtpInput(otp: String, onOtpChanged: (String) -> Unit, onConfirmOtp: () -> Un enabled = isLoginCodeValid(otp), modifier = Modifier.fillMaxWidth(), onClick = onConfirmOtp + ) { + Text(stringResource(R.string.do_continue)) + } +} + +@Composable +fun PasswordInput( + password: String, + onPasswordChanged: (String) -> Unit, + onConfirmPassword: () -> Unit +) { + val focusManager = LocalFocusManager.current + + Text(stringResource(R.string.enter_2fa_password)) + TextField( + password, + label = { Text(stringResource(R.string.password)) }, + placeholder = { Text(stringResource(R.string.password_example)) }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + modifier = Modifier.fillMaxWidth(), + onValueChange = onPasswordChanged + ) + Spacer(Modifier.height(16.dp)) + Button( + enabled = isPasswordValid(password), + modifier = Modifier.fillMaxWidth(), + onClick = onConfirmPassword ) { Text(stringResource(R.string.do_login)) } } @Composable -fun LoginScreen(onConfirmOtp: () -> Unit) { +fun LoginScreen(onLoginComplete: () -> Unit) { var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) } var phone by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var tokenPtr by remember { mutableStateOf(0L) } @@ -121,10 +157,21 @@ fun LoginScreen(onConfirmOtp: () -> Unit) { otp, onOtpChanged = { otp = it }, onConfirmOtp = { - Native.signIn(tokenPtr, otp) - onConfirmOtp() + val signInComplete = Native.signIn(tokenPtr, otp) + if (signInComplete) { + onLoginComplete() + } else { + stage = LoginStage.ASK_PASSWORD; + } } ) + LoginStage.ASK_PASSWORD -> PasswordInput( + password, + onPasswordChanged = { password = it }, + onConfirmPassword = { + Native.checkPassword(password) + onLoginComplete(); + }) } } } @@ -133,6 +180,6 @@ fun LoginScreen(onConfirmOtp: () -> Unit) { @Composable fun LoginPreview() { TalariaTheme { - LoginScreen(onConfirmOtp = { }) + LoginScreen(onLoginComplete = { }) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01c9a0e..7276ef2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,10 @@ Enter the code you received: Code 12345 + Enter your 2-factor authentication password: + Password + hunter2 + Continue Login Profile Picture Write a messageā€¦ diff --git a/native/src/lib.rs b/native/src/lib.rs index 249f743..757506c 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -4,12 +4,12 @@ use std::ffi::{CStr, CString}; use std::future::Future; -use grammers_client::{Client, Config}; -use grammers_client::types::{Dialog, LoginToken}; +use grammers_client::types::{Dialog, LoginToken, PasswordToken}; +use grammers_client::{Client, Config, SignInError}; use grammers_session::{PackedChat, Session}; -use jni::JNIEnv; use jni::objects::{JObject, JString}; use jni::sys::{jboolean, jint, jlong, jstring}; +use jni::JNIEnv; use log; use log::{error, info, Level}; use once_cell::sync::OnceCell; @@ -61,7 +61,7 @@ async fn init_client() -> Result<()> { api_hash: API_HASH.to_string(), params: Default::default(), }) - .await?; + .await?; info!("Connected!"); @@ -83,10 +83,31 @@ async fn request_login_code(phone: &str) -> Result { Ok(token) } -async fn sign_in(token: LoginToken, code: &str) -> Result<()> { +async fn sign_in(token: LoginToken, code: &str) -> Result { let client = CLIENT.get().ok_or("Client not initialized")?; - client.sign_in(&token, &code).await?; - Ok(()) + return match client.sign_in(&token, &code).await { + Err(SignInError::PasswordRequired(token)) => { + info!("Sign in 2fa password required. Hint: {:?}", token.hint()); + Ok(false) + } + Err(e) => { + client.sign_out().await?; + Err(Box::new(e)) + } + Ok(_) => Ok(true), + }; +} + +async fn check_password(password: &str) -> Result<()> { + let client = CLIENT.get().ok_or("Client not initialized")?; + let token = PasswordToken::new("hint"); + match client.check_password(token, &password).await { + Err(e) => { + client.sign_out().await?; + Err(Box::new(e)) + } + Ok(_) => Ok(()) + } } async fn get_dialogs() -> Result> { @@ -151,14 +172,46 @@ pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_signIn( _: JObject, token_ptr: jlong, code: JString, -) { +) -> jboolean { let token = *Box::from_raw(token_ptr as *mut LoginToken); let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr())); - match block_on(sign_in(token, code.to_str().unwrap())) { - Ok(_) => info!("Sign in success"), - Err(e) => error!("Failed to sign in: {}", e), - } + return match block_on(sign_in(token, code.to_str().unwrap())) { + Ok(sign_in_complete) => { + if sign_in_complete { + info!("Sign in success"); + } + sign_in_complete.into() + } + Err(e) => { + error!("Failed to sign in: {}", e); + true.into() + } + }; +} + +#[no_mangle] +pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_checkPassword( + env: JNIEnv, + _: JObject, + token_ptr: jlong, + code: JString, +) -> jboolean { + let token = *Box::from_raw(token_ptr as *mut LoginToken); + let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr())); + + return match block_on(sign_in(token, code.to_str().unwrap())) { + Ok(sign_in_complete) => { + if sign_in_complete { + info!("Sign in success"); + } + sign_in_complete.into() + } + Err(e) => { + error!("Failed to sign in: {}", e); + true.into() + } + }; } #[no_mangle]