Merge pull request #738 from jsoberg/#635-MoveAuthenticationToBackground
#635 move authentication to a background thread
This commit is contained in:
commit
edb01d819f
5 changed files with 471 additions and 117 deletions
|
@ -22,190 +22,306 @@
|
||||||
|
|
||||||
package org.shadowice.flocke.andotp.Activities;
|
package org.shadowice.flocke.andotp.Activities;
|
||||||
|
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import com.google.android.material.textfield.TextInputEditText;
|
import com.google.android.material.textfield.TextInputEditText;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||||
|
|
||||||
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.method.PasswordTransformationMethod;
|
import android.text.method.PasswordTransformationMethod;
|
||||||
import android.util.Base64;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewStub;
|
import android.view.ViewStub;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager.LayoutParams;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Hex;
|
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
|
||||||
import org.shadowice.flocke.andotp.R;
|
import org.shadowice.flocke.andotp.R;
|
||||||
|
import org.shadowice.flocke.andotp.Tasks.AuthenticationTask;
|
||||||
|
import org.shadowice.flocke.andotp.Tasks.AuthenticationTask.Result;
|
||||||
import org.shadowice.flocke.andotp.Utilities.Constants;
|
import org.shadowice.flocke.andotp.Utilities.Constants;
|
||||||
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
|
import static org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
|
||||||
|
|
||||||
public class AuthenticateActivity extends ThemedActivity
|
public class AuthenticateActivity extends BaseActivity
|
||||||
implements EditText.OnEditorActionListener, View.OnClickListener {
|
implements EditText.OnEditorActionListener, View.OnClickListener {
|
||||||
private String password;
|
|
||||||
|
|
||||||
AuthMethod authMethod;
|
private static final String TAG_TASK_FRAGMENT = "AuthenticateActivity.TaskFragmentTag";
|
||||||
String newEncryption = "";
|
|
||||||
boolean oldPassword = false;
|
|
||||||
|
|
||||||
TextInputEditText passwordInput;
|
private AuthMethod authMethod;
|
||||||
|
private String newEncryption = "";
|
||||||
|
private String existingAuthCredentials;
|
||||||
|
private boolean isAuthUpgrade = false;
|
||||||
|
private ProcessLifecycleObserver observer;
|
||||||
|
|
||||||
|
private TextInputLayout passwordLayout;
|
||||||
|
private TextInputEditText passwordInput;
|
||||||
|
private Button unlockButton;
|
||||||
|
private ProgressBar unlockProgress;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
if (! settings.getScreenshotsEnabled())
|
||||||
|
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
|
||||||
|
|
||||||
|
authMethod = settings.getAuthMethod();
|
||||||
|
newEncryption = getIntent().getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION);
|
||||||
|
existingAuthCredentials = settings.getAuthCredentials();
|
||||||
|
if (existingAuthCredentials.isEmpty()) {
|
||||||
|
existingAuthCredentials = settings.getOldCredentials(authMethod);
|
||||||
|
isAuthUpgrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our password is still empty at this point, we can't do anything.
|
||||||
|
if (existingAuthCredentials.isEmpty()) {
|
||||||
|
int missingPwResId = (authMethod == AuthMethod.PASSWORD)
|
||||||
|
? R.string.auth_toast_password_missing : R.string.auth_toast_pin_missing;
|
||||||
|
Toast.makeText(this, missingPwResId, Toast.LENGTH_LONG).show();
|
||||||
|
finishWithResult(true, null);
|
||||||
|
}
|
||||||
|
// If we're not using password or pin for auth method, we have nothing to authenticate here.
|
||||||
|
if (authMethod != AuthMethod.PASSWORD && authMethod != AuthMethod.PIN) {
|
||||||
|
finishWithResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
setTitle(R.string.auth_activity_title);
|
setTitle(R.string.auth_activity_title);
|
||||||
|
|
||||||
if (! settings.getScreenshotsEnabled())
|
|
||||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_container);
|
setContentView(R.layout.activity_container);
|
||||||
|
initToolbar();
|
||||||
|
initPasswordViews();
|
||||||
|
|
||||||
|
setBroadcastCallback(() -> {
|
||||||
|
if (settings.getRelockOnScreenOff()) {
|
||||||
|
cancelBackgroundTask();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer = new ProcessLifecycleObserver();
|
||||||
|
ProcessLifecycleOwner.get().getLifecycle()
|
||||||
|
.addObserver(observer);
|
||||||
|
|
||||||
|
getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initToolbar() {
|
||||||
Toolbar toolbar = findViewById(R.id.container_toolbar);
|
Toolbar toolbar = findViewById(R.id.container_toolbar);
|
||||||
toolbar.setNavigationIcon(null);
|
toolbar.setNavigationIcon(null);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPasswordViews() {
|
||||||
ViewStub stub = findViewById(R.id.container_stub);
|
ViewStub stub = findViewById(R.id.container_stub);
|
||||||
stub.setLayoutResource(R.layout.content_authenticate);
|
stub.setLayoutResource(R.layout.content_authenticate);
|
||||||
View v = stub.inflate();
|
View v = stub.inflate();
|
||||||
|
|
||||||
Intent callingIntent = getIntent();
|
initPasswordLabelView(v);
|
||||||
int labelMsg = callingIntent.getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate);
|
initPasswordLayoutView(v);
|
||||||
newEncryption = callingIntent.getStringExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION);
|
initPasswordInputView(v);
|
||||||
|
initUnlockViews(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPasswordLabelView(View v) {
|
||||||
|
int labelMsg = getIntent().getIntExtra(Constants.EXTRA_AUTH_MESSAGE, R.string.auth_msg_authenticate);
|
||||||
TextView passwordLabel = v.findViewById(R.id.passwordLabel);
|
TextView passwordLabel = v.findViewById(R.id.passwordLabel);
|
||||||
TextInputLayout passwordLayout = v.findViewById(R.id.passwordLayout);
|
|
||||||
passwordInput = v.findViewById(R.id.passwordEdit);
|
|
||||||
|
|
||||||
if (settings.getBlockAccessibility())
|
|
||||||
passwordLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && settings.getBlockAutofill())
|
|
||||||
passwordLayout.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
|
|
||||||
|
|
||||||
passwordLabel.setText(labelMsg);
|
passwordLabel.setText(labelMsg);
|
||||||
|
}
|
||||||
|
|
||||||
authMethod = settings.getAuthMethod();
|
private void initPasswordLayoutView(View v) {
|
||||||
password = settings.getAuthCredentials();
|
passwordLayout = v.findViewById(R.id.passwordLayout);
|
||||||
|
int hintResId = (authMethod == AuthMethod.PASSWORD) ? R.string.auth_hint_password : R.string.auth_hint_pin;
|
||||||
if (password.isEmpty()) {
|
passwordLayout.setHint(getString(hintResId));
|
||||||
password = settings.getOldCredentials(authMethod);
|
if (settings.getBlockAccessibility()) {
|
||||||
oldPassword = true;
|
passwordLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && settings.getBlockAutofill()) {
|
||||||
if (authMethod == AuthMethod.PASSWORD) {
|
passwordLayout.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);
|
||||||
if (password.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.auth_toast_password_missing, Toast.LENGTH_LONG).show();
|
|
||||||
finishWithResult(true, null);
|
|
||||||
} else {
|
|
||||||
passwordLayout.setHint(getString(R.string.auth_hint_password));
|
|
||||||
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
|
||||||
}
|
|
||||||
} else if (authMethod == AuthMethod.PIN) {
|
|
||||||
if (password.isEmpty()) {
|
|
||||||
Toast.makeText(this, R.string.auth_toast_pin_missing, Toast.LENGTH_LONG).show();
|
|
||||||
finishWithResult(true, null);
|
|
||||||
} else {
|
|
||||||
passwordLayout.setHint(getString(R.string.auth_hint_pin));
|
|
||||||
passwordInput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
finishWithResult(true, null);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPasswordInputView(View v) {
|
||||||
|
passwordInput = v.findViewById(R.id.passwordEdit);
|
||||||
|
int inputType = (authMethod == AuthMethod.PASSWORD)
|
||||||
|
? (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
|
||||||
|
: (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||||
|
passwordInput.setInputType(inputType);
|
||||||
passwordInput.setTransformationMethod(new PasswordTransformationMethod());
|
passwordInput.setTransformationMethod(new PasswordTransformationMethod());
|
||||||
passwordInput.setOnEditorActionListener(this);
|
passwordInput.setOnEditorActionListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
Button unlockButton = v.findViewById(R.id.buttonUnlock);
|
private void initUnlockViews(View v) {
|
||||||
|
unlockButton = v.findViewById(R.id.buttonUnlock);
|
||||||
unlockButton.setOnClickListener(this);
|
unlockButton.setOnClickListener(this);
|
||||||
|
unlockProgress = v.findViewById(R.id.unlockProgress);
|
||||||
|
}
|
||||||
|
|
||||||
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
private void cancelBackgroundTask() {
|
||||||
|
TaskFragment taskFragment = findTaskFragment();
|
||||||
|
if (taskFragment != null) {
|
||||||
|
taskFragment.task.cancel();
|
||||||
|
}
|
||||||
|
setupUiForTaskState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ProcessLifecycleObserver implements DefaultLifecycleObserver {
|
||||||
|
@Override
|
||||||
|
public void onStop(LifecycleOwner owner) {
|
||||||
|
if (settings.getRelockOnBackground()) {
|
||||||
|
cancelBackgroundTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
checkBackgroundTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkBackgroundTask() {
|
||||||
|
TaskFragment taskFragment = findTaskFragment();
|
||||||
|
if (taskFragment != null) {
|
||||||
|
if (taskFragment.task.isCanceled()) {
|
||||||
|
// The task was canceled, so remove the task fragment and reset password input.
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.remove(taskFragment)
|
||||||
|
.commit();
|
||||||
|
resetPasswordInput();
|
||||||
|
} else {
|
||||||
|
taskFragment.task.setCallback(this::handleResult);
|
||||||
|
setupUiForTaskState(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetPasswordInput() {
|
||||||
|
passwordInput.setText("");
|
||||||
|
passwordInput.requestFocus();
|
||||||
|
InputMethodManager keyboard = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
keyboard.showSoftInput(passwordInput, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private TaskFragment findTaskFragment() {
|
||||||
|
return (TaskFragment) getFragmentManager().findFragmentByTag(TAG_TASK_FRAGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupUiForTaskState(boolean isTaskRunning) {
|
||||||
|
passwordLayout.setEnabled(!isTaskRunning);
|
||||||
|
passwordInput.setEnabled(!isTaskRunning);
|
||||||
|
unlockButton.setEnabled(!isTaskRunning);
|
||||||
|
unlockButton.setVisibility(isTaskRunning? View.INVISIBLE : View.VISIBLE);
|
||||||
|
unlockProgress.setVisibility(isTaskRunning ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
checkPassword(passwordInput.getText().toString());
|
Editable text = passwordInput.getText();
|
||||||
|
startAuthTask(text != null ? text.toString() : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
checkPassword(v.getText().toString());
|
startAuthTask(v.getText().toString());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkPassword(String plainPassword) {
|
private void startAuthTask(String plainPassword) {
|
||||||
if (! oldPassword) {
|
TaskFragment taskFragment = findTaskFragment();
|
||||||
try {
|
// Don't start a task if we already have an active task running.
|
||||||
EncryptionHelper.PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations());
|
if (taskFragment == null || taskFragment.task.isCanceled()) {
|
||||||
byte[] passwordArray = Base64.decode(password, Base64.URL_SAFE);
|
AuthenticationTask task = new AuthenticationTask(this, isAuthUpgrade, existingAuthCredentials, plainPassword);
|
||||||
|
task.setCallback(this::handleResult);
|
||||||
|
|
||||||
if (Arrays.equals(passwordArray, credentials.password)) {
|
if (taskFragment == null) {
|
||||||
finishWithResult(true, credentials.key);
|
taskFragment = new TaskFragment();
|
||||||
} else {
|
getFragmentManager()
|
||||||
finishWithResult(false, null);
|
.beginTransaction()
|
||||||
}
|
.add(taskFragment, TAG_TASK_FRAGMENT)
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) {
|
.commit();
|
||||||
e.printStackTrace();
|
|
||||||
finishWithResult(false, null);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword)));
|
|
||||||
|
|
||||||
if (hashedPassword.equals(password)) {
|
|
||||||
byte[] key = settings.setAuthCredentials(plainPassword);
|
|
||||||
|
|
||||||
if (key == null)
|
|
||||||
Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show();
|
|
||||||
|
|
||||||
if (authMethod == AuthMethod.PASSWORD)
|
|
||||||
settings.removeAuthPasswordHash();
|
|
||||||
else if (authMethod == AuthMethod.PIN)
|
|
||||||
settings.removeAuthPINHash();
|
|
||||||
|
|
||||||
finishWithResult(true, key);
|
|
||||||
} else {
|
|
||||||
finishWithResult(false, null);
|
|
||||||
}
|
}
|
||||||
|
taskFragment.startTask(task);
|
||||||
|
setupUiForTaskState(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End with a result
|
private void handleResult(Result result) {
|
||||||
public void finishWithResult(boolean success, byte[] key) {
|
if (result.authUpgradeFailed) {
|
||||||
|
Toast.makeText(this, R.string.settings_toast_auth_upgrade_failed, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
finishWithResult(result.encryptionKey != null, result.encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishWithResult(boolean success, byte[] encryptionKey) {
|
||||||
Intent data = new Intent();
|
Intent data = new Intent();
|
||||||
|
if (newEncryption != null && !newEncryption.isEmpty())
|
||||||
if (newEncryption != null && ! newEncryption.isEmpty())
|
|
||||||
data.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEncryption);
|
data.putExtra(Constants.EXTRA_AUTH_NEW_ENCRYPTION, newEncryption);
|
||||||
|
if (encryptionKey != null)
|
||||||
if (key != null)
|
data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, encryptionKey);
|
||||||
data.putExtra(Constants.EXTRA_AUTH_PASSWORD_KEY, key);
|
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
setResult(RESULT_OK, data);
|
setResult(RESULT_OK, data);
|
||||||
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go back to the main activity
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
finishWithResult(false, null);
|
finishWithResult(false, null);
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
// We don't want the task to callback to a dead activity and cause a memory leak, so null it here.
|
||||||
|
TaskFragment taskFragment = findTaskFragment();
|
||||||
|
if (taskFragment != null) {
|
||||||
|
taskFragment.task.setCallback(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
ProcessLifecycleOwner.get().getLifecycle()
|
||||||
|
.removeObserver(observer);
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldDestroyOnScreenOff() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retained instance fragment to hold a running {@link AuthenticationTask} between configuration changes.*/
|
||||||
|
public static class TaskFragment extends Fragment {
|
||||||
|
|
||||||
|
AuthenticationTask task;
|
||||||
|
|
||||||
|
public TaskFragment() {
|
||||||
|
super();
|
||||||
|
setRetainInstance(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startTask(@NonNull AuthenticationTask task) {
|
||||||
|
this.task = task;
|
||||||
|
task.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,15 +43,9 @@ public abstract class BaseActivity extends ThemedActivity {
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
unregisterReceiver(screenOffReceiver);
|
unregisterReceiver(screenOffReceiver);
|
||||||
|
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void destroyIfNotMain() {
|
|
||||||
if (getClass() != MainActivity.class)
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBroadcastCallback(BroadcastReceivedCallback cb) {
|
public void setBroadcastCallback(BroadcastReceivedCallback cb) {
|
||||||
this.broadcastReceivedCallback = cb;
|
this.broadcastReceivedCallback = cb;
|
||||||
}
|
}
|
||||||
|
@ -62,14 +56,20 @@ public abstract class BaseActivity extends ThemedActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onReceive(Context context, Intent intent) {
|
||||||
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
|
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
|
||||||
if (broadcastReceivedCallback != null)
|
if (broadcastReceivedCallback != null) {
|
||||||
broadcastReceivedCallback.onReceivedScreenOff();
|
broadcastReceivedCallback.onReceivedScreenOff();
|
||||||
|
}
|
||||||
destroyIfNotMain();
|
if (shouldDestroyOnScreenOff()) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean shouldDestroyOnScreenOff() {
|
||||||
|
return getClass() != MainActivity.class;
|
||||||
|
}
|
||||||
|
|
||||||
interface BroadcastReceivedCallback {
|
interface BroadcastReceivedCallback {
|
||||||
void onReceivedScreenOff();
|
void onReceivedScreenOff();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
package org.shadowice.flocke.andotp.Tasks;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Hex;
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
import org.shadowice.flocke.andotp.Tasks.AuthenticationTask.Result;
|
||||||
|
import org.shadowice.flocke.andotp.Utilities.Constants.AuthMethod;
|
||||||
|
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
|
||||||
|
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper.PBKDF2Credentials;
|
||||||
|
import org.shadowice.flocke.andotp.Utilities.Settings;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class AuthenticationTask extends UiBasedBackgroundTask<Result> {
|
||||||
|
|
||||||
|
private final Settings settings;
|
||||||
|
|
||||||
|
private final boolean isAuthUpgrade;
|
||||||
|
private final String existingAuthCredentials;
|
||||||
|
private final String plainPassword;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context Context to be used to query settings (the application Context will be used to avoid memory leaks).
|
||||||
|
* @param isAuthUpgrade true if this is an authentication upgrade and new credentials should be saved, false if this is just confirmation.
|
||||||
|
* @param existingAuthCredentials The existing hashed authentication credentials that we have stored.
|
||||||
|
* @param plainPassword The plaintext user-entered password to check authentication with. */
|
||||||
|
public AuthenticationTask(Context context, boolean isAuthUpgrade, String existingAuthCredentials, String plainPassword) {
|
||||||
|
super(Result.failure());
|
||||||
|
Context applicationContext = context.getApplicationContext();
|
||||||
|
this.settings = new Settings(applicationContext);
|
||||||
|
|
||||||
|
this.isAuthUpgrade = isAuthUpgrade;
|
||||||
|
this.existingAuthCredentials = existingAuthCredentials;
|
||||||
|
this.plainPassword = plainPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
protected Result doInBackground() {
|
||||||
|
if (isAuthUpgrade) {
|
||||||
|
return upgradeAuthentication();
|
||||||
|
} else {
|
||||||
|
return confirmAuthentication();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Result upgradeAuthentication() {
|
||||||
|
String hashedPassword = new String(Hex.encodeHex(DigestUtils.sha256(plainPassword)));
|
||||||
|
if (!hashedPassword.equals(existingAuthCredentials))
|
||||||
|
return Result.failure();
|
||||||
|
|
||||||
|
byte[] key = settings.setAuthCredentials(plainPassword);
|
||||||
|
|
||||||
|
AuthMethod authMethod = settings.getAuthMethod();
|
||||||
|
if (authMethod == AuthMethod.PASSWORD)
|
||||||
|
settings.removeAuthPasswordHash();
|
||||||
|
else if (authMethod == AuthMethod.PIN)
|
||||||
|
settings.removeAuthPINHash();
|
||||||
|
|
||||||
|
if (key == null)
|
||||||
|
return Result.upgradeFailure();
|
||||||
|
else
|
||||||
|
return Result.success(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private Result confirmAuthentication() {
|
||||||
|
try {
|
||||||
|
PBKDF2Credentials credentials = EncryptionHelper.generatePBKDF2Credentials(plainPassword, settings.getSalt(), settings.getIterations());
|
||||||
|
byte[] passwordArray = Base64.decode(existingAuthCredentials, Base64.URL_SAFE);
|
||||||
|
|
||||||
|
if (Arrays.equals(passwordArray, credentials.password)) {
|
||||||
|
return Result.success(credentials.key);
|
||||||
|
}
|
||||||
|
return Result.failure();
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) {
|
||||||
|
Log.e("AuthenticationTask", "Problem decoding password", e);
|
||||||
|
return Result.failure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Result {
|
||||||
|
@Nullable
|
||||||
|
public final byte[] encryptionKey;
|
||||||
|
public final boolean authUpgradeFailed;
|
||||||
|
|
||||||
|
public Result(@Nullable byte[] encryptionKey, boolean authUpgradeFailed) {
|
||||||
|
this.encryptionKey = encryptionKey;
|
||||||
|
this.authUpgradeFailed = authUpgradeFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result success(byte[] encryptionKey) {
|
||||||
|
return new Result(encryptionKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result upgradeFailure() {
|
||||||
|
return new Result(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result failure() {
|
||||||
|
return new Result(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package org.shadowice.flocke.andotp.Tasks;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.AnyThread;
|
||||||
|
import androidx.annotation.MainThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/** Encapsulates a background task that needs to communicate back to the UI (on the main thread) to
|
||||||
|
* provide a result. */
|
||||||
|
public abstract class UiBasedBackgroundTask<Result> {
|
||||||
|
|
||||||
|
private final Result failedResult;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final Handler mainThreadHandler;
|
||||||
|
|
||||||
|
private final Object callbackLock = new Object();
|
||||||
|
@Nullable
|
||||||
|
private UiCallback<Result> callback;
|
||||||
|
@Nullable
|
||||||
|
private Result awaitedResult;
|
||||||
|
|
||||||
|
private volatile boolean isCanceled = false;
|
||||||
|
|
||||||
|
/** @param failedResult The result to return if the task fails (throws an exception or returns null). */
|
||||||
|
public UiBasedBackgroundTask(@NonNull Result failedResult) {
|
||||||
|
this.failedResult = failedResult;
|
||||||
|
this.executor = Executors.newSingleThreadExecutor();
|
||||||
|
this.mainThreadHandler = new Handler(Looper.getMainLooper());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param callback If null, any results which may arrive from a currently executing task will
|
||||||
|
* be stored until a new callback is set. */
|
||||||
|
public void setCallback(@Nullable UiCallback<Result> callback) {
|
||||||
|
synchronized (callbackLock) {
|
||||||
|
// Don't bother doing anything if the task was canceled.
|
||||||
|
if (isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.callback = callback;
|
||||||
|
// If we have an awaited result and are setting a new callback, publish the result immediately.
|
||||||
|
if (awaitedResult != null && callback != null) {
|
||||||
|
emitResultOnMainThread(callback, awaitedResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emitResultOnMainThread(@NonNull UiCallback<Result> callback, @NonNull Result result) {
|
||||||
|
mainThreadHandler.post(() -> callback.onResult(result));
|
||||||
|
this.callback = null;
|
||||||
|
this.awaitedResult = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Executed the task on a background thread. Safe to call from the main thread. */
|
||||||
|
@AnyThread
|
||||||
|
public void execute() {
|
||||||
|
executor.execute(this::runTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runTask() {
|
||||||
|
Result result = failedResult;
|
||||||
|
try {
|
||||||
|
result = doInBackground();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UiBasedBackgroundTask", "Problem running background task", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (callbackLock) {
|
||||||
|
// Don't bother issuing callback or storing result if this task is canceled.
|
||||||
|
if (isCanceled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (callback != null) {
|
||||||
|
emitResultOnMainThread(callback, result);
|
||||||
|
} else {
|
||||||
|
awaitedResult = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Work to be done in a background thread.
|
||||||
|
* @return Return the result from this task's execution.
|
||||||
|
* @throws Exception If an Exception is thrown from this task's execution, it will be logged
|
||||||
|
* and the provided default Result will be returned. */
|
||||||
|
@NonNull
|
||||||
|
protected abstract Result doInBackground() throws Exception;
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
public boolean isCanceled() {
|
||||||
|
return isCanceled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
public void cancel() {
|
||||||
|
isCanceled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface UiCallback<Result> {
|
||||||
|
@MainThread
|
||||||
|
void onResult(@NonNull Result result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,12 +34,27 @@
|
||||||
<requestFocus/>
|
<requestFocus/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<Button
|
<RelativeLayout
|
||||||
android:id="@+id/buttonUnlock"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end">
|
||||||
style="?android:attr/buttonBarButtonStyle"
|
|
||||||
android:text="@string/auth_button_unlock" />
|
<Button
|
||||||
|
android:id="@+id/buttonUnlock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:text="@string/auth_button_unlock" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/unlockProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_alignEnd="@id/buttonUnlock"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
Loading…
Reference in a new issue