diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ff4f1c0..4bf5b3a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -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" diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index a1726074..82bc40a8 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -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); } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java index b99a271b..cabcb830 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateReviewFragment.java @@ -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; diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java index 69cc5c84..b4473cad 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginActivity.java @@ -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 diff --git a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java index a7c3fb4f..881e8e33 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/LoginFragment.java @@ -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) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 4a2c8d71..abfd24f5 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -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 diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java new file mode 100644 index 00000000..46de0364 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java @@ -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); + } + +} diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java index b50fa1e4..510134d6 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/Helper.java @@ -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; + } + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java index ea49cd54..281fd257 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java +++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java @@ -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-"; } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_fingerprint.xml b/app/src/main/res/drawable/ic_fingerprint.xml new file mode 100644 index 00000000..81eccc55 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_generate.xml b/app/src/main/res/layout/fragment_generate.xml index 517ba60c..976448a2 100644 --- a/app/src/main/res/layout/fragment_generate.xml +++ b/app/src/main/res/layout/fragment_generate.xml @@ -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" diff --git a/app/src/main/res/layout/prompt_changepw.xml b/app/src/main/res/layout/prompt_changepw.xml index 94c3c33f..d66b2672 100644 --- a/app/src/main/res/layout/prompt_changepw.xml +++ b/app/src/main/res/layout/prompt_changepw.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/layout/prompt_password.xml b/app/src/main/res/layout/prompt_password.xml index d39604d1..eea8413e 100644 --- a/app/src/main/res/layout/prompt_password.xml +++ b/app/src/main/res/layout/prompt_password.xml @@ -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> \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 04176f38..af99e107 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -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> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0cbc3410..1cce7f6a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -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> diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 79ecb73c..7b2e7ebe 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -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> diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1793bb05..b04945bb 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -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> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aac66986..93ea2cb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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>