WIP implement 2fa login

2fa-support
expectocode 2022-10-13 00:16:11 +01:00
parent e1985e8c73
commit 0fef9a8fec
5 changed files with 125 additions and 18 deletions

View File

@ -72,7 +72,7 @@ fun TalariaApp() {
ChatScreen(selectedDialog) ChatScreen(selectedDialog)
} }
composable(route = TalariaScreen.Login.name) { composable(route = TalariaScreen.Login.name) {
LoginScreen(onConfirmOtp = { LoginScreen(onLoginComplete = {
navController.navigate(TalariaScreen.Dialog.name) navController.navigate(TalariaScreen.Dialog.name)
}) })
} }

View File

@ -8,7 +8,10 @@ object Native {
external fun initClient() external fun initClient()
external fun needLogin(): Boolean external fun needLogin(): Boolean
external fun requestLoginCode(phone: String): Long 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 getDialogs(): Long
external fun dialogCount(dialogsPtr: Long): Int external fun dialogCount(dialogsPtr: Long): Int
external fun dialogPacked(dialogsPtr: Long, index: Int): String external fun dialogPacked(dialogsPtr: Long, index: Int): String

View File

@ -24,10 +24,12 @@ import dev.lonami.talaria.ui.theme.TalariaTheme
enum class LoginStage { enum class LoginStage {
ASK_PHONE, ASK_PHONE,
ASK_CODE, ASK_CODE,
ASK_PASSWORD,
} }
fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty() fun isPhoneValid(phone: String): Boolean = phone.trim('+', ' ').isNotEmpty()
fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5 fun isLoginCodeValid(code: String): Boolean = code.trim().count { it.isDigit() } == 5
fun isPasswordValid(password: String): Boolean = password.isNotEmpty()
@Composable @Composable
fun PhoneInput(phone: String, onPhoneChanged: (String) -> Unit, onSendCode: () -> Unit) { 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), enabled = isLoginCodeValid(otp),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onConfirmOtp 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)) Text(stringResource(R.string.do_login))
} }
} }
@Composable @Composable
fun LoginScreen(onConfirmOtp: () -> Unit) { fun LoginScreen(onLoginComplete: () -> Unit) {
var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) } var stage by remember { mutableStateOf(LoginStage.ASK_PHONE) }
var phone by remember { mutableStateOf("") } var phone by remember { mutableStateOf("") }
var otp by remember { mutableStateOf("") } var otp by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var tokenPtr by remember { mutableStateOf(0L) } var tokenPtr by remember { mutableStateOf(0L) }
@ -121,10 +157,21 @@ fun LoginScreen(onConfirmOtp: () -> Unit) {
otp, otp,
onOtpChanged = { otp = it }, onOtpChanged = { otp = it },
onConfirmOtp = { onConfirmOtp = {
Native.signIn(tokenPtr, otp) val signInComplete = Native.signIn(tokenPtr, otp)
onConfirmOtp() 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 @Composable
fun LoginPreview() { fun LoginPreview() {
TalariaTheme { TalariaTheme {
LoginScreen(onConfirmOtp = { }) LoginScreen(onLoginComplete = { })
} }
} }

View File

@ -8,6 +8,10 @@
<string name="enter_otp">Enter the code you received:</string> <string name="enter_otp">Enter the code you received:</string>
<string name="otp">Code</string> <string name="otp">Code</string>
<string name="otp_example">12345</string> <string name="otp_example">12345</string>
<string name="enter_2fa_password">Enter your 2-factor authentication password:</string>
<string name="password">Password</string>
<string name="password_example">hunter2</string>
<string name="do_continue">Continue</string>
<string name="do_login">Login</string> <string name="do_login">Login</string>
<string name="profile_photo">Profile Picture</string> <string name="profile_photo">Profile Picture</string>
<string name="write_message">Write a message…</string> <string name="write_message">Write a message…</string>

View File

@ -4,12 +4,12 @@
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::future::Future; use std::future::Future;
use grammers_client::{Client, Config}; use grammers_client::types::{Dialog, LoginToken, PasswordToken};
use grammers_client::types::{Dialog, LoginToken}; use grammers_client::{Client, Config, SignInError};
use grammers_session::{PackedChat, Session}; use grammers_session::{PackedChat, Session};
use jni::JNIEnv;
use jni::objects::{JObject, JString}; use jni::objects::{JObject, JString};
use jni::sys::{jboolean, jint, jlong, jstring}; use jni::sys::{jboolean, jint, jlong, jstring};
use jni::JNIEnv;
use log; use log;
use log::{error, info, Level}; use log::{error, info, Level};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -61,7 +61,7 @@ async fn init_client() -> Result<()> {
api_hash: API_HASH.to_string(), api_hash: API_HASH.to_string(),
params: Default::default(), params: Default::default(),
}) })
.await?; .await?;
info!("Connected!"); info!("Connected!");
@ -83,10 +83,31 @@ async fn request_login_code(phone: &str) -> Result<LoginToken> {
Ok(token) Ok(token)
} }
async fn sign_in(token: LoginToken, code: &str) -> Result<()> { async fn sign_in(token: LoginToken, code: &str) -> Result<bool> {
let client = CLIENT.get().ok_or("Client not initialized")?; let client = CLIENT.get().ok_or("Client not initialized")?;
client.sign_in(&token, &code).await?; return match client.sign_in(&token, &code).await {
Ok(()) 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<Vec<Dialog>> { async fn get_dialogs() -> Result<Vec<Dialog>> {
@ -151,14 +172,46 @@ pub unsafe extern "C" fn Java_dev_lonami_talaria_bindings_Native_signIn(
_: JObject, _: JObject,
token_ptr: jlong, token_ptr: jlong,
code: JString, code: JString,
) { ) -> jboolean {
let token = *Box::from_raw(token_ptr as *mut LoginToken); let token = *Box::from_raw(token_ptr as *mut LoginToken);
let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr())); let code = CString::from(CStr::from_ptr(env.get_string(code).unwrap().as_ptr()));
match block_on(sign_in(token, code.to_str().unwrap())) { return match block_on(sign_in(token, code.to_str().unwrap())) {
Ok(_) => info!("Sign in success"), Ok(sign_in_complete) => {
Err(e) => error!("Failed to sign in: {}", e), 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] #[no_mangle]