Compare commits

...

3 Commits

Author SHA1 Message Date
expectocode 0fef9a8fec WIP implement 2fa login 2022-10-13 08:18:51 +01:00
expectocode e1985e8c73 Mark gradlew as executable 2022-10-12 22:36:57 +01:00
tan 5cff07bc6e Add a touch more detail to readme instructions (#1)
Co-authored-by: expectocode <expectocode@gmail.com>
Reviewed-on: Lonami/Talaria#1
2022-10-12 23:25:41 +02:00
7 changed files with 133 additions and 18 deletions

View File

@ -1,5 +1,7 @@
# Building # Building
Clone the repo, then open the project in Android Studio.
Make sure the required Android NDK platforms are installed, and the environment Make sure the required Android NDK platforms are installed, and the environment
variable `ANDROID_NDK_TOOLCHAIN_DIR` is configured correctly. variable `ANDROID_NDK_TOOLCHAIN_DIR` is configured correctly.
@ -8,3 +10,9 @@ On Windows, this might be a path such as the following (NDK "Side by side" SDK t
``` ```
%LOCALAPPDATA%\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin %LOCALAPPDATA%\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\bin
``` ```
Set your API ID and hash in the rust code.
Change the Cargo.toml to point to a local grammers source tree and remove the version argument.
Sync gradle files, and Android Studio's "build" and "run" should Just Work.

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>

0
gradlew vendored Normal file → Executable file
View File

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]