From a2d6ca07405a4754e6b3c6e98f8f87eb466afabd Mon Sep 17 00:00:00 2001 From: 0140454 Date: Wed, 25 Apr 2018 04:34:39 +0800 Subject: [PATCH] Implement fingerprint login method (#244) * Implement fingerprint login method * Display a security warning * Verify the identity before sensitive operation --- app/src/main/AndroidManifest.xml | 1 + .../m2049r/xmrwallet/GenerateFragment.java | 49 +++++- .../xmrwallet/GenerateReviewFragment.java | 56 +++++- .../com/m2049r/xmrwallet/LoginActivity.java | 141 +++------------ .../com/m2049r/xmrwallet/LoginFragment.java | 13 ++ .../com/m2049r/xmrwallet/WalletActivity.java | 33 +++- .../xmrwallet/util/FingerprintHelper.java | 40 +++++ .../com/m2049r/xmrwallet/util/Helper.java | 160 ++++++++++++++++++ .../m2049r/xmrwallet/util/KeyStoreHelper.java | 68 +++++++- app/src/main/res/drawable/ic_fingerprint.xml | 37 ++++ app/src/main/res/layout/fragment_generate.xml | 22 +++ app/src/main/res/layout/prompt_changepw.xml | 20 +++ app/src/main/res/layout/prompt_password.xml | 10 ++ app/src/main/res/values-es/strings.xml | 15 ++ app/src/main/res/values-it/strings.xml | 15 ++ app/src/main/res/values-nb/strings.xml | 15 ++ app/src/main/res/values-zh-rTW/strings.xml | 12 ++ app/src/main/res/values/strings.xml | 15 ++ 18 files changed, 588 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/FingerprintHelper.java create mode 100644 app/src/main/res/drawable/ic_fingerprint.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ff4f1c..4bf5b3a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + 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 4a2c8d7..abfd24f 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 0000000..46de036 --- /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 b50fa1e..510134d 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 ea49cd5..281fd25 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 0000000..81eccc5 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ 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 517ba60..976448a 100644 --- a/app/src/main/res/layout/fragment_generate.xml +++ b/app/src/main/res/layout/fragment_generate.xml @@ -1,6 +1,7 @@ @@ -55,6 +56,27 @@ + + + + + + + + + + + + + + \ 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 d39604d..eea8413 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" /> + + \ 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 04176f3..af99e10 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -91,7 +91,9 @@ Nueva contraseña para %1$s Repetir contraseña para %1$s Contraseña para %1$s + [You can also open wallet using fingerprint.\nPlease touch sensor.] Confirmar Contraseña + [Fingerprint not recognized. Try again.] ¡Contraseña incorrecta! ¡El monedero no existe! ¡Esto no es un monedero! @@ -140,6 +142,19 @@ Crear monedero Nombre del monedero Frase de Contraseña + [Allow to open wallet using fingerprint] + [Fingerprint Authentication +

With fingerprint authentication enabled, you can view wallet balance and receive funds + without entering password.

+

But for additional security, monerujo will still require you to enter password when + viewing wallet details or sending funds.

+ Security Warning +

Finally, monerujo wants to remind you that anyone who can get your fingerprint will be + able to peep into your wallet balance.

+

For instance, a malicious user around you can open your wallet when you are asleep.

+ Are you sure to enable this function? + ]]>]
Contraseñas no coinciden Contraseña no puede estar vacía ¡Házme ya un monedero! diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0cbc341..1cce7f6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -155,7 +155,9 @@ [New Passphrase for %1$s] [Repeat Passphrase for %1$s] Password per %1$s + [You can also open wallet using fingerprint.\nPlease touch sensor.] Conferma Password + [Fingerprint not recognized. Try again.] Password errata! Il portafoglio non esiste! Questo non è un portafoglio! @@ -207,6 +209,19 @@ Crea portafoglio Nome del portafoglio Passphrase del portafoglio + [Allow to open wallet using fingerprint] + [Fingerprint Authentication +

With fingerprint authentication enabled, you can view wallet balance and receive funds + without entering password.

+

But for additional security, monerujo will still require you to enter password when + viewing wallet details or sending funds.

+ Security Warning +

Finally, monerujo wants to remind you that anyone who can get your fingerprint will be + able to peep into your wallet balance.

+

For instance, a malicious user around you can open your wallet when you are asleep.

+ Are you sure to enable this function? + ]]>]
[Passphrases do not match] [Passphrase may not be empty] Fammi subito un portafoglio! diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 79ecb73..7b2e7eb 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -153,7 +153,9 @@ [Nytt passord for %1$s] [Gjenta passord for %1$s] Passord for %1$s + [You can also open wallet using fingerprint.\nPlease touch sensor.] Bekreft passord + [Fingerprint not recognized. Try again.] Feil passord! Lommebok eksisterer ikke! Dette er ikke en lommebok! @@ -205,6 +207,19 @@ Lag lommebok Lommeboknavn Lommebokpassord + [Allow to open wallet using fingerprint] + [Fingerprint Authentication +

With fingerprint authentication enabled, you can view wallet balance and receive funds + without entering password.

+

But for additional security, monerujo will still require you to enter password when + viewing wallet details or sending funds.

+ Security Warning +

Finally, monerujo wants to remind you that anyone who can get your fingerprint will be + able to peep into your wallet balance.

+

For instance, a malicious user around you can open your wallet when you are asleep.

+ Are you sure to enable this function? + ]]>]
[Passordene stemmer ikke overens] [Passord kan ikke være tomt] Lag meg en lommebok! diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1793bb0..b04945b 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 @@ 為 %1$s 設定新密碼 重複輸入 %1$s 的密碼 %1$s 的密碼 + 你也可以使用指紋來開啟錢包。\n請輕觸你的指紋感應器。 確認密碼 + 無法辨識的指紋,請再試一次。 密碼錯誤! 錢包不存在! 這不是錢包! @@ -207,6 +209,16 @@ 建立錢包 錢包名稱 錢包密碼 + 允許使用指紋開啟錢包 + 指紋驗證 +

啟用指紋驗證後,您可以觀看錢包餘額並接收資金,而無需輸入密碼。

+

但為了提高安全性,monerujo 仍然會要求您在觀看錢包詳細資訊或發送資金時輸入密碼。

+ 安全警告 +

最後,monerujo 想提醒您,任何可以取得您指紋的人都能夠窺視您的錢包餘額。

+

例如,您周遭的惡意使用者可以趁您睡著時使用您的指紋開啟錢包。

+ 您確定要啟用本功能嗎? + ]]>
密碼不符 密碼不得空白 建立錢包! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aac6698..93ea2cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,7 +158,9 @@ New Passphrase for %1$s Repeat Passphrase for %1$s Password for %1$s + You can also open wallet using fingerprint.\nPlease touch sensor. Confirm Password + Fingerprint not recognized. Try again. Incorrect password! Wallet does not exist! This is not a wallet! @@ -211,6 +213,19 @@ Create Wallet Wallet Name Wallet Passphrase + Allow to open wallet using fingerprint + Fingerprint Authentication +

With fingerprint authentication enabled, you can view wallet balance and receive funds + without entering password.

+

But for additional security, monerujo will still require you to enter password when + viewing wallet details or sending funds.

+ Security Warning +

Finally, monerujo wants to remind you that anyone who can get your fingerprint will be + able to peep into your wallet balance.

+

For instance, a malicious user around you can open your wallet when you are asleep.

+ Are you sure to enable this function? + ]]>
Passphrases do not match Passphrase may not be empty Make me a wallet already!