mirror of https://github.com/m2049r/xmrwallet.git
Implement fingerprint login method (#244)
* Implement fingerprint login method * Display a security warning * Verify the identity before sensitive operation
This commit is contained in:
parent
1115bbb706
commit
a2d6ca0740
|
@ -7,6 +7,7 @@
|
|||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
package com.m2049r.xmrwallet;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
|
@ -32,21 +35,23 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.FingerprintHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.nulabinc.zxcvbn.Strength;
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
@ -60,6 +65,7 @@ public class GenerateFragment extends Fragment {
|
|||
|
||||
private TextInputLayout etWalletName;
|
||||
private TextInputLayout etWalletPassword;
|
||||
private LinearLayout llFingerprintAuth;
|
||||
private TextInputLayout etWalletAddress;
|
||||
private TextInputLayout etWalletMnemonic;
|
||||
private TextInputLayout etWalletViewKey;
|
||||
|
@ -80,6 +86,7 @@ public class GenerateFragment extends Fragment {
|
|||
|
||||
etWalletName = (TextInputLayout) view.findViewById(R.id.etWalletName);
|
||||
etWalletPassword = (TextInputLayout) view.findViewById(R.id.etWalletPassword);
|
||||
llFingerprintAuth = (LinearLayout) view.findViewById(R.id.llFingerprintAuth);
|
||||
etWalletMnemonic = (TextInputLayout) view.findViewById(R.id.etWalletMnemonic);
|
||||
etWalletAddress = (TextInputLayout) view.findViewById(R.id.etWalletAddress);
|
||||
etWalletViewKey = (TextInputLayout) view.findViewById(R.id.etWalletViewKey);
|
||||
|
@ -147,6 +154,30 @@ public class GenerateFragment extends Fragment {
|
|||
}
|
||||
});
|
||||
|
||||
if (FingerprintHelper.isDeviceSupported(getContext())) {
|
||||
llFingerprintAuth.setVisibility(View.VISIBLE);
|
||||
|
||||
final Switch swFingerprintAllowed = (Switch) llFingerprintAuth.getChildAt(0);
|
||||
swFingerprintAllowed.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (!swFingerprintAllowed.isChecked()) return;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
swFingerprintAllowed.setChecked(false);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (type.equals(TYPE_NEW)) {
|
||||
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
etWalletPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
|
@ -424,6 +455,7 @@ public class GenerateFragment extends Fragment {
|
|||
|
||||
String name = etWalletName.getEditText().getText().toString();
|
||||
String password = etWalletPassword.getEditText().getText().toString();
|
||||
boolean fingerprintAuthAllowed = ((Switch) llFingerprintAuth.getChildAt(0)).isChecked();
|
||||
|
||||
// create the real wallet password
|
||||
String crazyPass = KeyStoreHelper.getCrazyPass(getActivity(), password);
|
||||
|
@ -433,11 +465,17 @@ public class GenerateFragment extends Fragment {
|
|||
|
||||
if (type.equals(TYPE_NEW)) {
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(getActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerate(name, crazyPass);
|
||||
} else if (type.equals(TYPE_SEED)) {
|
||||
if (!checkMnemonic()) return;
|
||||
String seed = etWalletMnemonic.getEditText().getText().toString();
|
||||
bGenerate.setEnabled(false);
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(getActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerate(name, crazyPass, seed, height);
|
||||
} else if (type.equals(TYPE_KEY) || type.equals(TYPE_VIEWONLY)) {
|
||||
if (checkAddress() && checkViewKey() && checkSpendKey()) {
|
||||
|
@ -448,6 +486,9 @@ public class GenerateFragment extends Fragment {
|
|||
if (type.equals(TYPE_KEY)) {
|
||||
spendKey = etWalletSpendKey.getEditText().getText().toString();
|
||||
}
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(getActivity(), name, password);
|
||||
}
|
||||
activityCallback.onGenerate(name, crazyPass, address, viewKey, spendKey, height);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.support.annotation.Nullable;
|
|||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -38,18 +39,21 @@ import android.widget.ImageButton;
|
|||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.FingerprintHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.KeyStoreException;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
@ -369,12 +373,21 @@ public class GenerateReviewFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... params) {
|
||||
if (params.length != 3) return false;
|
||||
if (params.length != 4) return false;
|
||||
File walletFile = Helper.getWalletFile(getActivity(), params[0]);
|
||||
String oldPassword = params[1];
|
||||
String userPassword = params[2];
|
||||
boolean fingerprintAuthAllowed = Boolean.valueOf(params[3]);
|
||||
newPassword = KeyStoreHelper.getCrazyPass(getActivity(), userPassword);
|
||||
return changeWalletPassword(newPassword);
|
||||
boolean success = changeWalletPassword(newPassword);
|
||||
if (success) {
|
||||
if (fingerprintAuthAllowed) {
|
||||
KeyStoreHelper.saveWalletUserPass(getActivity(), walletName, userPassword);
|
||||
} else {
|
||||
KeyStoreHelper.removeWalletUserPass(getActivity(), walletName);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -410,6 +423,37 @@ public class GenerateReviewFragment extends Fragment {
|
|||
final TextInputLayout etPasswordB = (TextInputLayout) promptsView.findViewById(R.id.etWalletPasswordB);
|
||||
etPasswordB.setHint(getString(R.string.prompt_changepwB, walletName));
|
||||
|
||||
LinearLayout llFingerprintAuth = (LinearLayout) promptsView.findViewById(R.id.llFingerprintAuth);
|
||||
final Switch swFingerprintAllowed = (Switch) llFingerprintAuth.getChildAt(0);
|
||||
if (FingerprintHelper.isDeviceSupported(getActivity())) {
|
||||
llFingerprintAuth.setVisibility(View.VISIBLE);
|
||||
|
||||
swFingerprintAllowed.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (!swFingerprintAllowed.isChecked()) return;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setMessage(Html.fromHtml(getString(R.string.generate_fingerprint_warn)))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
swFingerprintAllowed.setChecked(false);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
swFingerprintAllowed.setChecked(FingerprintHelper.isFingerprintAuthAllowed(walletName));
|
||||
} catch (KeyStoreException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
etPasswordA.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
@ -483,7 +527,7 @@ public class GenerateReviewFragment extends Fragment {
|
|||
} else if (!newPasswordA.equals(newPasswordB)) {
|
||||
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
|
||||
} else if (newPasswordA.equals(newPasswordB)) {
|
||||
new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA);
|
||||
new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
|
||||
Helper.hideKeyboardAlways(getActivity());
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
|
@ -505,7 +549,7 @@ public class GenerateReviewFragment extends Fragment {
|
|||
} else if (!newPasswordA.equals(newPasswordB)) {
|
||||
etPasswordB.setError(getString(R.string.generate_bad_passwordB));
|
||||
} else if (newPasswordA.equals(newPasswordB)) {
|
||||
new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA);
|
||||
new AsyncChangePassword().execute(walletName, walletPassword, newPasswordA, Boolean.toString(swFingerprintAllowed.isChecked()));
|
||||
Helper.hideKeyboardAlways(getActivity());
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
|
|
|
@ -29,18 +29,14 @@ import android.os.AsyncTask;
|
|||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
@ -54,7 +50,7 @@ import com.m2049r.xmrwallet.model.NetworkType;
|
|||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.WalletService;
|
||||
import com.m2049r.xmrwallet.util.CrazyPassEncoder;
|
||||
import com.m2049r.xmrwallet.util.FingerprintHelper;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
||||
|
@ -67,6 +63,7 @@ import java.io.IOException;
|
|||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.Date;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
@ -179,9 +176,9 @@ public class LoginActivity extends SecureActivity
|
|||
case DialogInterface.BUTTON_POSITIVE:
|
||||
final File walletFile = Helper.getWalletFile(LoginActivity.this, walletName);
|
||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||
promptPassword(walletName, new PasswordAction() {
|
||||
Helper.promptPassword(LoginActivity.this, walletName, true, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void action(String walletName, String password) {
|
||||
public void action(String walletName, String password, boolean fingerprintUsed) {
|
||||
startDetails(walletFile, password, GenerateReviewFragment.VIEW_TYPE_DETAILS);
|
||||
}
|
||||
});
|
||||
|
@ -211,9 +208,9 @@ public class LoginActivity extends SecureActivity
|
|||
if (checkServiceRunning()) return;
|
||||
final File walletFile = Helper.getWalletFile(this, walletName);
|
||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||
promptPassword(walletName, new PasswordAction() {
|
||||
Helper.promptPassword(LoginActivity.this, walletName, false, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void action(String walletName, String password) {
|
||||
public void action(String walletName, String password, boolean fingerprintUsed) {
|
||||
startReceive(walletFile, password);
|
||||
}
|
||||
});
|
||||
|
@ -234,7 +231,17 @@ public class LoginActivity extends SecureActivity
|
|||
if (params.length != 2) return false;
|
||||
File walletFile = Helper.getWalletFile(LoginActivity.this, params[0]);
|
||||
String newName = params[1];
|
||||
return renameWallet(walletFile, newName);
|
||||
boolean success = renameWallet(walletFile, newName);
|
||||
try {
|
||||
if (success && FingerprintHelper.isFingerprintAuthAllowed(params[0])) {
|
||||
String savedPass = KeyStoreHelper.loadWalletUserPass(LoginActivity.this, params[0]);
|
||||
KeyStoreHelper.saveWalletUserPass(LoginActivity.this, newName, savedPass);
|
||||
KeyStoreHelper.removeWalletUserPass(LoginActivity.this, params[0]);
|
||||
}
|
||||
} catch (KeyStoreException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -381,6 +388,7 @@ public class LoginActivity extends SecureActivity
|
|||
if (params.length != 1) return false;
|
||||
String walletName = params[0];
|
||||
if (backupWallet(walletName) && deleteWallet(Helper.getWalletFile(LoginActivity.this, walletName))) {
|
||||
KeyStoreHelper.removeWalletUserPass(LoginActivity.this, walletName);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -460,110 +468,6 @@ public class LoginActivity extends SecureActivity
|
|||
startGenerateFragment(type);
|
||||
}
|
||||
|
||||
AlertDialog openDialog = null; // for preventing opening of multiple dialogs
|
||||
|
||||
void promptPassword(final String wallet, final PasswordAction action) {
|
||||
if (openDialog != null) return; // we are already asking for password
|
||||
Context context = LoginActivity.this;
|
||||
LayoutInflater li = LayoutInflater.from(context);
|
||||
View promptsView = li.inflate(R.layout.prompt_password, null);
|
||||
|
||||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context);
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword);
|
||||
etPassword.setHint(LoginActivity.this.getString(R.string.prompt_password, wallet));
|
||||
|
||||
etPassword.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (etPassword.getError() != null) {
|
||||
etPassword.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.label_ok), null)
|
||||
.setNegativeButton(getString(R.string.label_cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Helper.hideKeyboardAlways(LoginActivity.this);
|
||||
dialog.cancel();
|
||||
openDialog = null;
|
||||
}
|
||||
});
|
||||
openDialog = alertDialogBuilder.create();
|
||||
|
||||
openDialog.setOnShowListener(new DialogInterface.OnShowListener() {
|
||||
@Override
|
||||
public void onShow(DialogInterface dialog) {
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (processPasswordEntry(wallet, pass, action)) {
|
||||
Helper.hideKeyboardAlways(LoginActivity.this);
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
etPassword.setError(getString(R.string.bad_password));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// accept keyboard "ok"
|
||||
etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (processPasswordEntry(wallet, pass, action)) {
|
||||
Helper.hideKeyboardAlways(LoginActivity.this);
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
etPassword.setError(getString(R.string.bad_password));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Helper.showKeyboard(openDialog);
|
||||
openDialog.show();
|
||||
}
|
||||
|
||||
interface PasswordAction {
|
||||
void action(String walletName, String password);
|
||||
}
|
||||
|
||||
private boolean processPasswordEntry(String walletName, String pass, PasswordAction action) {
|
||||
String walletPassword = Helper.getWalletPassword(getApplicationContext(), walletName, pass);
|
||||
if (walletPassword != null) {
|
||||
action.action(walletName, walletPassword);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// LoginFragment.Listener
|
||||
////////////////////////////////////////
|
||||
|
@ -699,11 +603,12 @@ public class LoginActivity extends SecureActivity
|
|||
}
|
||||
}
|
||||
|
||||
void startWallet(String walletName, String walletPassword) {
|
||||
void startWallet(String walletName, String walletPassword, boolean fingerprintUsed) {
|
||||
Timber.d("startWallet()");
|
||||
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
|
||||
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
|
||||
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
|
||||
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -1193,10 +1098,10 @@ public class LoginActivity extends SecureActivity
|
|||
File walletFile = Helper.getWalletFile(this, walletNode.getName());
|
||||
if (WalletManager.getInstance().walletExists(walletFile)) {
|
||||
WalletManager.getInstance().setDaemon(walletNode);
|
||||
promptPassword(walletNode.getName(), new PasswordAction() {
|
||||
Helper.promptPassword(LoginActivity.this, walletNode.getName(), false, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void action(String walletName, String password) {
|
||||
startWallet(walletName, password);
|
||||
public void action(String walletName, String password, boolean fingerprintUsed) {
|
||||
startWallet(walletName, password, fingerprintUsed);
|
||||
}
|
||||
});
|
||||
} else { // this cannot really happen as we prefilter choices
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
|
|||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.NodeList;
|
||||
import com.m2049r.xmrwallet.util.Notice;
|
||||
import com.m2049r.xmrwallet.widget.DropDownEditText;
|
||||
|
@ -61,6 +62,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
|
|||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
@ -311,6 +313,17 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
|||
ivGunther.setImageDrawable(null);
|
||||
}
|
||||
}
|
||||
|
||||
// remove information of non-existent wallet
|
||||
Set<String> removedWallets = getActivity()
|
||||
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getAll().keySet();
|
||||
for (WalletManager.WalletInfo s : walletList) {
|
||||
removedWallets.remove(s.name);
|
||||
}
|
||||
for (String name : removedWallets) {
|
||||
KeyStoreHelper.removeWalletUserPass(getActivity(), name);
|
||||
}
|
||||
}
|
||||
|
||||
private void showInfo(@NonNull String name) {
|
||||
|
|
|
@ -49,8 +49,6 @@ import com.m2049r.xmrwallet.util.Helper;
|
|||
import com.m2049r.xmrwallet.util.UserNotes;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class WalletActivity extends SecureActivity implements WalletFragment.Listener,
|
||||
|
@ -62,8 +60,10 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
|
|||
|
||||
public static final String REQUEST_ID = "id";
|
||||
public static final String REQUEST_PW = "pw";
|
||||
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
|
||||
|
||||
private Toolbar toolbar;
|
||||
private boolean needVerifyIdentity;
|
||||
|
||||
@Override
|
||||
public void setToolbarButton(int type) {
|
||||
|
@ -120,6 +120,7 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
|
|||
acquireWakeLock();
|
||||
String walletId = extras.getString(REQUEST_ID);
|
||||
String walletPassword = extras.getString(REQUEST_PW);
|
||||
needVerifyIdentity = extras.getBoolean(REQUEST_FINGERPRINT_USED);
|
||||
connectWalletService(walletId, walletPassword);
|
||||
} else {
|
||||
finish();
|
||||
|
@ -397,7 +398,17 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
|
|||
|
||||
@Override
|
||||
public void onSendRequest() {
|
||||
replaceFragment(new SendFragment(), null, null);
|
||||
if (needVerifyIdentity) {
|
||||
Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void action(String walletName, String password, boolean fingerprintUsed) {
|
||||
replaceFragment(new SendFragment(), null, null);
|
||||
needVerifyIdentity = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
replaceFragment(new SendFragment(), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -697,10 +708,22 @@ public class WalletActivity extends SecureActivity implements WalletFragment.Lis
|
|||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
Bundle extras = new Bundle();
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString("type", GenerateReviewFragment.VIEW_TYPE_WALLET);
|
||||
extras.putString("password", getIntent().getExtras().getString(REQUEST_PW));
|
||||
replaceFragment(new GenerateReviewFragment(), null, extras);
|
||||
|
||||
if (needVerifyIdentity) {
|
||||
Helper.promptPassword(WalletActivity.this, getWallet().getName(), true, new Helper.PasswordAction() {
|
||||
@Override
|
||||
public void action(String walletName, String password, boolean fingerprintUsed) {
|
||||
replaceFragment(new GenerateReviewFragment(), null, extras);
|
||||
needVerifyIdentity = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
replaceFragment(new GenerateReviewFragment(), null, extras);
|
||||
}
|
||||
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
// do nothing
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
|
||||
public class FingerprintHelper {
|
||||
|
||||
public static boolean isDeviceSupported(Context context) {
|
||||
FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context);
|
||||
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
|
||||
|
||||
return keyguardManager != null &&
|
||||
keyguardManager.isKeyguardSecure() &&
|
||||
fingerprintManager.isHardwareDetected() &&
|
||||
fingerprintManager.hasEnrolledFingerprints();
|
||||
}
|
||||
|
||||
public static boolean isFingerprintAuthAllowed(String wallet) throws KeyStoreException {
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStoreHelper.SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
|
||||
try {
|
||||
keyStore.load(null);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not load KeyStore", ex);
|
||||
}
|
||||
|
||||
return keyStore.containsAlias(KeyStoreHelper.SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet);
|
||||
}
|
||||
|
||||
public static void authenticate(Context context, CancellationSignal cancelSignal,
|
||||
FingerprintManagerCompat.AuthenticationCallback callback) {
|
||||
FingerprintManagerCompat manager = FingerprintManagerCompat.from(context);
|
||||
manager.authenticate(null, 0, cancelSignal, callback, null);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,10 +18,12 @@ package com.m2049r.xmrwallet.util;
|
|||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
@ -30,13 +32,24 @@ import android.graphics.drawable.BitmapDrawable;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.os.Environment;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
|
||||
import android.support.v4.os.CancellationSignal;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
|
@ -51,6 +64,7 @@ import java.math.BigInteger;
|
|||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.security.KeyStoreException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
@ -340,4 +354,150 @@ public class Helper {
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
static AlertDialog openDialog = null; // for preventing opening of multiple dialogs
|
||||
|
||||
static public void promptPassword(final Context context, final String wallet, boolean fingerprintDisabled, final PasswordAction action) {
|
||||
if (openDialog != null) return; // we are already asking for password
|
||||
LayoutInflater li = LayoutInflater.from(context);
|
||||
final View promptsView = li.inflate(R.layout.prompt_password, null);
|
||||
|
||||
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context);
|
||||
alertDialogBuilder.setView(promptsView);
|
||||
|
||||
final TextInputLayout etPassword = (TextInputLayout) promptsView.findViewById(R.id.etPassword);
|
||||
etPassword.setHint(context.getString(R.string.prompt_password, wallet));
|
||||
|
||||
boolean fingerprintAuthCheck;
|
||||
try {
|
||||
fingerprintAuthCheck = FingerprintHelper.isFingerprintAuthAllowed(wallet);
|
||||
} catch (KeyStoreException ex) {
|
||||
fingerprintAuthCheck = false;
|
||||
}
|
||||
|
||||
final boolean fingerprintAuthAllowed = !fingerprintDisabled && fingerprintAuthCheck;
|
||||
final CancellationSignal cancelSignal = new CancellationSignal();
|
||||
|
||||
if (fingerprintAuthAllowed) {
|
||||
promptsView.findViewById(R.id.txtFingerprintAuth).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
etPassword.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (etPassword.getError() != null) {
|
||||
etPassword.setError(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start,
|
||||
int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start,
|
||||
int before, int count) {
|
||||
}
|
||||
});
|
||||
|
||||
// set dialog message
|
||||
alertDialogBuilder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(context.getString(R.string.label_ok), null)
|
||||
.setNegativeButton(context.getString(R.string.label_cancel),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
cancelSignal.cancel();
|
||||
dialog.cancel();
|
||||
openDialog = null;
|
||||
}
|
||||
});
|
||||
openDialog = alertDialogBuilder.create();
|
||||
|
||||
final FingerprintManagerCompat.AuthenticationCallback fingerprintAuthCallback = new FingerprintManagerCompat.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
||||
((TextView) promptsView.findViewById(R.id.txtFingerprintAuth)).setText(errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
||||
String userPass = KeyStoreHelper.loadWalletUserPass(context, wallet);
|
||||
if (Helper.processPasswordEntry(context, wallet, userPass, true, action)) {
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
etPassword.setError(context.getString(R.string.bad_password));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
((TextView) promptsView.findViewById(R.id.txtFingerprintAuth))
|
||||
.setText(context.getString(R.string.bad_fingerprint));
|
||||
}
|
||||
};
|
||||
|
||||
openDialog.setOnShowListener(new DialogInterface.OnShowListener() {
|
||||
@Override
|
||||
public void onShow(DialogInterface dialog) {
|
||||
if (fingerprintAuthAllowed) {
|
||||
FingerprintHelper.authenticate(context, cancelSignal, fingerprintAuthCallback);
|
||||
}
|
||||
Button button = ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (processPasswordEntry(context, wallet, pass, false, action)) {
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
etPassword.setError(context.getString(R.string.bad_password));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// accept keyboard "ok"
|
||||
etPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
String pass = etPassword.getEditText().getText().toString();
|
||||
if (processPasswordEntry(context, wallet, pass, false, action)) {
|
||||
Helper.hideKeyboardAlways((Activity) context);
|
||||
openDialog.dismiss();
|
||||
openDialog = null;
|
||||
} else {
|
||||
etPassword.setError(context.getString(R.string.bad_password));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Helper.showKeyboard(openDialog);
|
||||
openDialog.show();
|
||||
}
|
||||
|
||||
public interface PasswordAction {
|
||||
void action(String walletName, String password, boolean fingerprintUsed);
|
||||
}
|
||||
|
||||
static private boolean processPasswordEntry(Context context, String walletName, String pass, boolean fingerprintUsed, PasswordAction action) {
|
||||
String walletPassword = Helper.getWalletPassword(context, walletName, pass);
|
||||
if (walletPassword != null) {
|
||||
action.action(walletName, walletPassword, fingerprintUsed);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import android.os.Build;
|
|||
import android.security.KeyPairGeneratorSpec;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
@ -35,11 +36,13 @@ import java.security.KeyStoreException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
@ -71,6 +74,41 @@ public class KeyStoreHelper {
|
|||
return CrazyPassEncoder.encode(cnSlowHash(sig));
|
||||
}
|
||||
|
||||
public static void saveWalletUserPass(Context context, String wallet, String password) {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
byte[] data = password.getBytes(StandardCharsets.UTF_8);
|
||||
try {
|
||||
KeyStoreHelper.createKeys(context, walletKeyAlias);
|
||||
byte[] encrypted = KeyStoreHelper.encrypt(walletKeyAlias, data);
|
||||
context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putString(wallet, Base64.encodeToString(encrypted, Base64.DEFAULT))
|
||||
.apply();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static String loadWalletUserPass(Context context, String wallet) {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(wallet, "");
|
||||
byte[] data = Base64.decode(encoded, Base64.DEFAULT);
|
||||
byte[] decrypted = KeyStoreHelper.decrypt(walletKeyAlias, data);
|
||||
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static void removeWalletUserPass(Context context, String wallet) {
|
||||
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
|
||||
try {
|
||||
KeyStoreHelper.deleteKeys(walletKeyAlias);
|
||||
context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.remove(wallet).apply();
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a public and private key and stores it using the Android Key
|
||||
* Store, so that only this application will be able to access the keys.
|
||||
|
@ -132,9 +170,10 @@ public class KeyStoreHelper {
|
|||
KeyProperties.KEY_ALGORITHM_RSA, SecurityConstants.KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
|
||||
keyPairGenerator.initialize(
|
||||
new KeyGenParameterSpec.Builder(
|
||||
alias, KeyProperties.PURPOSE_SIGN)
|
||||
alias, KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
|
||||
.build());
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
Timber.d("M Keys created");
|
||||
|
@ -166,6 +205,30 @@ public class KeyStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static byte[] encrypt(String alias, byte[] data) {
|
||||
try {
|
||||
PublicKey publicKey = getPrivateKeyEntry(alias).getCertificate().getPublicKey();
|
||||
Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return cipher.doFinal(data);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not initialize RSA cipher", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] decrypt(String alias, byte[] data) {
|
||||
try {
|
||||
PrivateKey privateKey = getPrivateKeyEntry(alias).getPrivateKey();
|
||||
Cipher cipher = Cipher.getInstance(SecurityConstants.CIPHER_RSA_ECB_PKCS1);
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
return cipher.doFinal(data);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("Could not initialize RSA cipher", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the data using the key pair stored in the Android Key Store. This
|
||||
* signature can be used with the data later to verify it was signed by this
|
||||
|
@ -213,5 +276,8 @@ public class KeyStoreHelper {
|
|||
String KEYSTORE_PROVIDER_ANDROID_KEYSTORE = "AndroidKeyStore";
|
||||
String TYPE_RSA = "RSA";
|
||||
String SIGNATURE_SHA256withRSA = "SHA256withRSA";
|
||||
String CIPHER_RSA_ECB_PKCS1 = "RSA/ECB/PKCS1Padding";
|
||||
String WALLET_PASS_PREFS_NAME = "wallet";
|
||||
String WALLET_PASS_KEY_PREFIX = "walletKey-";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<!--
|
||||
Copyright (C) 2015 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32.0"
|
||||
android:viewportHeight="32.0">
|
||||
|
||||
<path
|
||||
android:fillColor="#6b8693"
|
||||
android:pathData="M16,16m -16, 0a 16, 16 0 1, 0 32, 0a 16, 16 0 1, 0 -32, 0" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M23.7,5.9c-0.1,0.0 -0.2,0.0 -0.3,-0.1C21.0,4.5 18.6,3.9 16.0,3.9c-2.5,0.0 -4.6,0.6 -6.9,1.9C8.8,6.0 8.3,5.9 8.1,5.5C7.9,5.2 8.0,4.7 8.4,4.5c2.5,-1.4 4.9,-2.1 7.7,-2.1c2.8,0.0 5.4,0.7 8.0,2.1c0.4,0.2 0.5,0.6 0.3,1.0C24.2,5.7 24.0,5.9 23.7,5.9z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M5.3,13.2c-0.1,0.0 -0.3,0.0 -0.4,-0.1c-0.3,-0.2 -0.4,-0.7 -0.2,-1.0c1.3,-1.9 2.9,-3.4 4.9,-4.5c4.1,-2.2 9.3,-2.2 13.4,0.0c1.9,1.1 3.6,2.5 4.9,4.4c0.2,0.3 0.1,0.8 -0.2,1.0c-0.3,0.2 -0.8,0.1 -1.0,-0.2c-1.2,-1.7 -2.6,-3.0 -4.3,-4.0c-3.7,-2.0 -8.3,-2.0 -12.0,0.0c-1.7,0.9 -3.2,2.3 -4.3,4.0C5.7,13.1 5.5,13.2 5.3,13.2z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M13.3,29.6c-0.2,0.0 -0.4,-0.1 -0.5,-0.2c-1.1,-1.2 -1.7,-2.0 -2.6,-3.6c-0.9,-1.7 -1.4,-3.7 -1.4,-5.9c0.0,-4.1 3.3,-7.4 7.4,-7.4c4.1,0.0 7.4,3.3 7.4,7.4c0.0,0.4 -0.3,0.7 -0.7,0.7s-0.7,-0.3 -0.7,-0.7c0.0,-3.3 -2.7,-5.9 -5.9,-5.9c-3.3,0.0 -5.9,2.7 -5.9,5.9c0.0,2.0 0.4,3.8 1.2,5.2c0.8,1.6 1.4,2.2 2.4,3.3c0.3,0.3 0.3,0.8 0.0,1.0C13.7,29.5 13.5,29.6 13.3,29.6z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M22.6,27.1c-1.6,0.0 -2.9,-0.4 -4.1,-1.2c-1.9,-1.4 -3.1,-3.6 -3.1,-6.0c0.0,-0.4 0.3,-0.7 0.7,-0.7s0.7,0.3 0.7,0.7c0.0,1.9 0.9,3.7 2.5,4.8c0.9,0.6 1.9,1.0 3.2,1.0c0.3,0.0 0.8,0.0 1.3,-0.1c0.4,-0.1 0.8,0.2 0.8,0.6c0.1,0.4 -0.2,0.8 -0.6,0.8C23.4,27.1 22.8,27.1 22.6,27.1z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M20.0,29.9c-0.1,0.0 -0.1,0.0 -0.2,0.0c-2.1,-0.6 -3.4,-1.4 -4.8,-2.9c-1.8,-1.9 -2.8,-4.4 -2.8,-7.1c0.0,-2.2 1.8,-4.1 4.1,-4.1c2.2,0.0 4.1,1.8 4.1,4.1c0.0,1.4 1.2,2.6 2.6,2.6c1.4,0.0 2.6,-1.2 2.6,-2.6c0.0,-5.1 -4.2,-9.3 -9.3,-9.3c-3.6,0.0 -6.9,2.1 -8.4,5.4C7.3,17.1 7.0,18.4 7.0,19.8c0.0,1.1 0.1,2.7 0.9,4.9c0.1,0.4 -0.1,0.8 -0.4,0.9c-0.4,0.1 -0.8,-0.1 -0.9,-0.4c-0.6,-1.8 -0.9,-3.6 -0.9,-5.4c0.0,-1.6 0.3,-3.1 0.9,-4.4c1.7,-3.8 5.6,-6.3 9.8,-6.3c5.9,0.0 10.7,4.8 10.7,10.7c0.0,2.2 -1.8,4.1 -4.1,4.1s-4.0,-1.8 -4.0,-4.1c0.0,-1.4 -1.2,-2.6 -2.6,-2.6c-1.4,0.0 -2.6,1.2 -2.6,2.6c0.0,2.3 0.9,4.5 2.4,6.1c1.2,1.3 2.4,2.0 4.2,2.5c0.4,0.1 0.6,0.5 0.5,0.9C20.6,29.7 20.3,29.9 20.0,29.9z"/>
|
||||
</vector>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp">
|
||||
|
@ -55,6 +56,27 @@
|
|||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llFingerprintAuth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="16dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/generate_fingerprint_hint"
|
||||
android:textSize="18sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/etWalletMnemonic"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -40,4 +40,24 @@
|
|||
android:textAlignment="textStart" />
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llFingerprintAuth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone">
|
||||
|
||||
<Switch
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/generate_fingerprint_hint"
|
||||
android:textSize="18sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -22,4 +22,14 @@
|
|||
android:inputType="textPassword" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtFingerprintAuth"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="10dp"
|
||||
android:drawableStart="@drawable/ic_fingerprint"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/prompt_fingerprint_auth"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
|
@ -91,7 +91,9 @@
|
|||
<string name="prompt_changepw">Nueva contraseña para %1$s</string>
|
||||
<string name="prompt_changepwB">Repetir contraseña para %1$s</string>
|
||||
<string name="prompt_password">Contraseña para %1$s</string>
|
||||
<string name="prompt_fingerprint_auth">[You can also open wallet using fingerprint.\nPlease touch sensor.]</string>
|
||||
<string name="prompt_send_password">Confirmar Contraseña</string>
|
||||
<string name="bad_fingerprint">[Fingerprint not recognized. Try again.]</string>
|
||||
<string name="bad_password">¡Contraseña incorrecta!</string>
|
||||
<string name="bad_wallet">¡El monedero no existe!</string>
|
||||
<string name="error_not_wallet">¡Esto no es un monedero!</string>
|
||||
|
@ -140,6 +142,19 @@
|
|||
<string name="generate_title">Crear monedero</string>
|
||||
<string name="generate_name_hint">Nombre del monedero</string>
|
||||
<string name="generate_password_hint">Frase de Contraseña</string>
|
||||
<string name="generate_fingerprint_hint">[Allow to open wallet using fingerprint]</string>
|
||||
<string name="generate_fingerprint_warn">[<![CDATA[
|
||||
<strong>Fingerprint Authentication</strong>
|
||||
<p>With fingerprint authentication enabled, you can view wallet balance and receive funds
|
||||
without entering password.</p>
|
||||
<p>But for additional security, monerujo will still require you to enter password when
|
||||
viewing wallet details or sending funds.</p>
|
||||
<strong>Security Warning</strong>
|
||||
<p>Finally, monerujo wants to remind you that anyone who can get your fingerprint will be
|
||||
able to peep into your wallet balance.</p>
|
||||
<p>For instance, a malicious user around you can open your wallet when you are asleep.</p>
|
||||
<strong>Are you sure to enable this function?</strong>
|
||||
]]>]</string>
|
||||
<string name="generate_bad_passwordB">Contraseñas no coinciden</string>
|
||||
<string name="generate_empty_passwordB">Contraseña no puede estar vacía</string>
|
||||
<string name="generate_buttonGenerate">¡Házme ya un monedero!</string>
|
||||
|
|
|
@ -155,7 +155,9 @@
|
|||
<string name="prompt_changepw">[New Passphrase for %1$s]</string>
|
||||
<string name="prompt_changepwB">[Repeat Passphrase for %1$s]</string>
|
||||
<string name="prompt_password">Password per %1$s</string>
|
||||
<string name="prompt_fingerprint_auth">[You can also open wallet using fingerprint.\nPlease touch sensor.]</string>
|
||||
<string name="prompt_send_password">Conferma Password</string>
|
||||
<string name="bad_fingerprint">[Fingerprint not recognized. Try again.]</string>
|
||||
<string name="bad_password">Password errata!</string>
|
||||
<string name="bad_wallet">Il portafoglio non esiste!</string>
|
||||
<string name="error_not_wallet">Questo non è un portafoglio!</string>
|
||||
|
@ -207,6 +209,19 @@
|
|||
<string name="generate_title">Crea portafoglio</string>
|
||||
<string name="generate_name_hint">Nome del portafoglio</string>
|
||||
<string name="generate_password_hint">Passphrase del portafoglio</string>
|
||||
<string name="generate_fingerprint_hint">[Allow to open wallet using fingerprint]</string>
|
||||
<string name="generate_fingerprint_warn">[<![CDATA[
|
||||
<strong>Fingerprint Authentication</strong>
|
||||
<p>With fingerprint authentication enabled, you can view wallet balance and receive funds
|
||||
without entering password.</p>
|
||||
<p>But for additional security, monerujo will still require you to enter password when
|
||||
viewing wallet details or sending funds.</p>
|
||||
<strong>Security Warning</strong>
|
||||
<p>Finally, monerujo wants to remind you that anyone who can get your fingerprint will be
|
||||
able to peep into your wallet balance.</p>
|
||||
<p>For instance, a malicious user around you can open your wallet when you are asleep.</p>
|
||||
<strong>Are you sure to enable this function?</strong>
|
||||
]]>]</string>
|
||||
<string name="generate_bad_passwordB">[Passphrases do not match]</string>
|
||||
<string name="generate_empty_passwordB">[Passphrase may not be empty]</string>
|
||||
<string name="generate_buttonGenerate">Fammi subito un portafoglio!</string>
|
||||
|
|
|
@ -153,7 +153,9 @@
|
|||
<string name="prompt_changepw">[Nytt passord for %1$s]</string>
|
||||
<string name="prompt_changepwB">[Gjenta passord for %1$s]</string>
|
||||
<string name="prompt_password">Passord for %1$s</string>
|
||||
<string name="prompt_fingerprint_auth">[You can also open wallet using fingerprint.\nPlease touch sensor.]</string>
|
||||
<string name="prompt_send_password">Bekreft passord</string>
|
||||
<string name="bad_fingerprint">[Fingerprint not recognized. Try again.]</string>
|
||||
<string name="bad_password">Feil passord!</string>
|
||||
<string name="bad_wallet">Lommebok eksisterer ikke!</string>
|
||||
<string name="error_not_wallet">Dette er ikke en lommebok!</string>
|
||||
|
@ -205,6 +207,19 @@
|
|||
<string name="generate_title">Lag lommebok</string>
|
||||
<string name="generate_name_hint">Lommeboknavn</string>
|
||||
<string name="generate_password_hint">Lommebokpassord</string>
|
||||
<string name="generate_fingerprint_hint">[Allow to open wallet using fingerprint]</string>
|
||||
<string name="generate_fingerprint_warn">[<![CDATA[
|
||||
<strong>Fingerprint Authentication</strong>
|
||||
<p>With fingerprint authentication enabled, you can view wallet balance and receive funds
|
||||
without entering password.</p>
|
||||
<p>But for additional security, monerujo will still require you to enter password when
|
||||
viewing wallet details or sending funds.</p>
|
||||
<strong>Security Warning</strong>
|
||||
<p>Finally, monerujo wants to remind you that anyone who can get your fingerprint will be
|
||||
able to peep into your wallet balance.</p>
|
||||
<p>For instance, a malicious user around you can open your wallet when you are asleep.</p>
|
||||
<strong>Are you sure to enable this function?</strong>
|
||||
]]>]</string>
|
||||
<string name="generate_bad_passwordB">[Passordene stemmer ikke overens]</string>
|
||||
<string name="generate_empty_passwordB">[Passord kan ikke være tomt]</string>
|
||||
<string name="generate_buttonGenerate">Lag meg en lommebok!</string>
|
||||
|
|
|
@ -155,7 +155,9 @@
|
|||
<string name="prompt_changepw">為 %1$s 設定新密碼</string>
|
||||
<string name="prompt_changepwB">重複輸入 %1$s 的密碼</string>
|
||||
<string name="prompt_password">%1$s 的密碼</string>
|
||||
<string name="prompt_fingerprint_auth">你也可以使用指紋來開啟錢包。\n請輕觸你的指紋感應器。</string>
|
||||
<string name="prompt_send_password">確認密碼</string>
|
||||
<string name="bad_fingerprint">無法辨識的指紋,請再試一次。</string>
|
||||
<string name="bad_password">密碼錯誤!</string>
|
||||
<string name="bad_wallet">錢包不存在!</string>
|
||||
<string name="error_not_wallet">這不是錢包!</string>
|
||||
|
@ -207,6 +209,16 @@
|
|||
<string name="generate_title">建立錢包</string>
|
||||
<string name="generate_name_hint">錢包名稱</string>
|
||||
<string name="generate_password_hint">錢包密碼</string>
|
||||
<string name="generate_fingerprint_hint">允許使用指紋開啟錢包</string>
|
||||
<string name="generate_fingerprint_warn"><![CDATA[
|
||||
<strong>指紋驗證</strong>
|
||||
<p>啟用指紋驗證後,您可以觀看錢包餘額並接收資金,而無需輸入密碼。</p>
|
||||
<p>但為了提高安全性,monerujo 仍然會要求您在觀看錢包詳細資訊或發送資金時輸入密碼。</p>
|
||||
<strong>安全警告</strong>
|
||||
<p>最後,monerujo 想提醒您,任何可以取得您指紋的人都能夠窺視您的錢包餘額。</p>
|
||||
<p>例如,您周遭的惡意使用者可以趁您睡著時使用您的指紋開啟錢包。</p>
|
||||
<strong>您確定要啟用本功能嗎?</strong>
|
||||
]]></string>
|
||||
<string name="generate_bad_passwordB">密碼不符</string>
|
||||
<string name="generate_empty_passwordB">密碼不得空白</string>
|
||||
<string name="generate_buttonGenerate">建立錢包!</string>
|
||||
|
|
|
@ -158,7 +158,9 @@
|
|||
<string name="prompt_changepw">New Passphrase for %1$s</string>
|
||||
<string name="prompt_changepwB">Repeat Passphrase for %1$s</string>
|
||||
<string name="prompt_password">Password for %1$s</string>
|
||||
<string name="prompt_fingerprint_auth">You can also open wallet using fingerprint.\nPlease touch sensor.</string>
|
||||
<string name="prompt_send_password">Confirm Password</string>
|
||||
<string name="bad_fingerprint">Fingerprint not recognized. Try again.</string>
|
||||
<string name="bad_password">Incorrect password!</string>
|
||||
<string name="bad_wallet">Wallet does not exist!</string>
|
||||
<string name="error_not_wallet">This is not a wallet!</string>
|
||||
|
@ -211,6 +213,19 @@
|
|||
<string name="generate_title">Create Wallet</string>
|
||||
<string name="generate_name_hint">Wallet Name</string>
|
||||
<string name="generate_password_hint">Wallet Passphrase</string>
|
||||
<string name="generate_fingerprint_hint">Allow to open wallet using fingerprint</string>
|
||||
<string name="generate_fingerprint_warn"><![CDATA[
|
||||
<strong>Fingerprint Authentication</strong>
|
||||
<p>With fingerprint authentication enabled, you can view wallet balance and receive funds
|
||||
without entering password.</p>
|
||||
<p>But for additional security, monerujo will still require you to enter password when
|
||||
viewing wallet details or sending funds.</p>
|
||||
<strong>Security Warning</strong>
|
||||
<p>Finally, monerujo wants to remind you that anyone who can get your fingerprint will be
|
||||
able to peep into your wallet balance.</p>
|
||||
<p>For instance, a malicious user around you can open your wallet when you are asleep.</p>
|
||||
<strong>Are you sure to enable this function?</strong>
|
||||
]]></string>
|
||||
<string name="generate_bad_passwordB">Passphrases do not match</string>
|
||||
<string name="generate_empty_passwordB">Passphrase may not be empty</string>
|
||||
<string name="generate_buttonGenerate">Make me a wallet already!</string>
|
||||
|
|
Loading…
Reference in New Issue