mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-30 04:24:07 -04:00
Add native Android password unlock flow (#1776)
This commit is contained in:
@@ -58,6 +58,14 @@
|
||||
android:theme="@style/PasskeyRegistrationTheme"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<!-- Password Unlock Activity -->
|
||||
<activity
|
||||
android:name=".passwordunlock.PasswordUnlockActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/PasskeyRegistrationTheme"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustPan" />
|
||||
|
||||
<!-- QR Scanner Activity -->
|
||||
<activity
|
||||
android:name=".qrscanner.QRScannerActivity"
|
||||
|
||||
@@ -106,16 +106,17 @@ class MainActivity : ReactActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity results - specifically for PIN unlock and PIN setup.
|
||||
* Handle activity results.
|
||||
*/
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
// Handle PIN unlock results directly
|
||||
if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_UNLOCK_REQUEST_CODE) {
|
||||
handlePinUnlockResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_SETUP_REQUEST_CODE) {
|
||||
handlePinSetupResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PASSWORD_UNLOCK_REQUEST_CODE) {
|
||||
handlePasswordUnlockResult(resultCode, data)
|
||||
} else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.QR_SCANNER_REQUEST_CODE) {
|
||||
handleQRScannerResult(resultCode, data)
|
||||
}
|
||||
@@ -184,7 +185,7 @@ class MainActivity : ReactActivity() {
|
||||
/**
|
||||
* Handle PIN setup result.
|
||||
* @param resultCode The result code from the PIN setup activity.
|
||||
* @param data The intent data (not used for setup, setup happens internally).
|
||||
* @param data The intent data
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun handlePinSetupResult(resultCode: Int, data: Intent?) {
|
||||
@@ -210,6 +211,35 @@ class MainActivity : ReactActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle password unlock result.
|
||||
* @param resultCode The result code from the password unlock activity.
|
||||
* @param data The intent data containing the encryption key.
|
||||
*/
|
||||
private fun handlePasswordUnlockResult(resultCode: Int, data: Intent?) {
|
||||
val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.passwordUnlockPromise
|
||||
net.aliasvault.app.nativevaultmanager.NativeVaultManager.passwordUnlockPromise = null
|
||||
|
||||
if (promise == null) {
|
||||
return
|
||||
}
|
||||
|
||||
when (resultCode) {
|
||||
net.aliasvault.app.passwordunlock.PasswordUnlockActivity.RESULT_SUCCESS -> {
|
||||
val encryptionKeyBase64 = data?.getStringExtra(
|
||||
net.aliasvault.app.passwordunlock.PasswordUnlockActivity.EXTRA_ENCRYPTION_KEY,
|
||||
)
|
||||
promise.resolve(encryptionKeyBase64)
|
||||
}
|
||||
net.aliasvault.app.passwordunlock.PasswordUnlockActivity.RESULT_CANCELLED -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
else -> {
|
||||
promise.resolve(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle QR scanner result.
|
||||
* @param resultCode The result code from the QR scanner activity.
|
||||
|
||||
@@ -68,6 +68,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
*/
|
||||
const val QR_SCANNER_REQUEST_CODE = 1003
|
||||
|
||||
/**
|
||||
* Request code for password unlock activity.
|
||||
*/
|
||||
const val PASSWORD_UNLOCK_REQUEST_CODE = 1004
|
||||
|
||||
/**
|
||||
* Static holder for the pending promise from showPinUnlock.
|
||||
* This allows MainActivity to resolve/reject the promise directly without
|
||||
@@ -83,6 +88,14 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
*/
|
||||
@Volatile
|
||||
var pinSetupPromise: Promise? = null
|
||||
|
||||
/**
|
||||
* Static holder for the pending promise from showPasswordUnlock.
|
||||
* This allows MainActivity to resolve/reject the promise directly without
|
||||
* depending on React context availability.
|
||||
*/
|
||||
@Volatile
|
||||
var passwordUnlockPromise: Promise? = null
|
||||
}
|
||||
|
||||
private val vaultStore = VaultStore.getInstance(
|
||||
@@ -1410,14 +1423,35 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user using biometric or PIN unlock.
|
||||
* This method automatically detects which authentication method is enabled and uses it.
|
||||
* Returns true if authentication succeeded, false otherwise.
|
||||
* Show native password unlock screen.
|
||||
* Returns encryption key (base64) if successful, null if cancelled.
|
||||
*
|
||||
* @param title The title for authentication. If null or empty, uses default.
|
||||
* @param subtitle The subtitle for authentication. If null or empty, uses default.
|
||||
* @param promise The promise to resolve with authentication result.
|
||||
* @param title Custom title for the password unlock screen. If null or empty, uses default.
|
||||
* @param subtitle Custom subtitle for the password unlock screen. If null or empty, uses default.
|
||||
* @param promise The promise to resolve with encryption key or null.
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun showPasswordUnlock(title: String?, subtitle: String?, promise: Promise) {
|
||||
val activity = currentActivity
|
||||
if (activity == null) {
|
||||
promise.reject("NO_ACTIVITY", "No activity available", null)
|
||||
return
|
||||
}
|
||||
|
||||
// Store promise in static companion object so MainActivity can resolve it directly
|
||||
passwordUnlockPromise = promise
|
||||
|
||||
// Launch password unlock activity
|
||||
val intent = Intent(activity, net.aliasvault.app.passwordunlock.PasswordUnlockActivity::class.java)
|
||||
if (!title.isNullOrEmpty()) {
|
||||
intent.putExtra(net.aliasvault.app.passwordunlock.PasswordUnlockActivity.EXTRA_CUSTOM_TITLE, title)
|
||||
}
|
||||
if (!subtitle.isNullOrEmpty()) {
|
||||
intent.putExtra(net.aliasvault.app.passwordunlock.PasswordUnlockActivity.EXTRA_CUSTOM_SUBTITLE, subtitle)
|
||||
}
|
||||
activity.startActivityForResult(intent, PASSWORD_UNLOCK_REQUEST_CODE)
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
override fun scanQRCode(prefixes: ReadableArray?, statusText: String?, promise: Promise) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
@@ -1491,10 +1525,23 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
// Use biometric authentication
|
||||
try {
|
||||
val authenticated = vaultStore.issueBiometricAuthentication(title)
|
||||
promise.resolve(authenticated)
|
||||
if (!authenticated) {
|
||||
// Biometric failed - reject with error instead of resolving false
|
||||
promise.reject(
|
||||
"AUTH_ERROR",
|
||||
"No authentication method available. Please enable PIN or biometric unlock in settings.",
|
||||
null,
|
||||
)
|
||||
} else {
|
||||
promise.resolve(authenticated)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Biometric authentication failed", e)
|
||||
promise.resolve(false)
|
||||
promise.reject(
|
||||
"AUTH_ERROR",
|
||||
"Biometric authentication failed: ${e.message}",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
package net.aliasvault.app.passwordunlock
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.aliasvault.app.R
|
||||
import net.aliasvault.app.vaultstore.VaultStore
|
||||
import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider
|
||||
import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider
|
||||
|
||||
/**
|
||||
* Native password unlock activity.
|
||||
* This activity presents a password entry UI for vault unlocking.
|
||||
*
|
||||
* Result:
|
||||
* - RESULT_SUCCESS: Password verified, encryption key returned in EXTRA_ENCRYPTION_KEY
|
||||
* - RESULT_CANCELLED: User cancelled
|
||||
*/
|
||||
class PasswordUnlockActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
/** Result code for successful password unlock. */
|
||||
const val RESULT_SUCCESS = Activity.RESULT_OK
|
||||
|
||||
/** Result code for cancelled password unlock. */
|
||||
const val RESULT_CANCELLED = Activity.RESULT_CANCELED
|
||||
|
||||
/** Intent extra key for the encryption key (returned on success). */
|
||||
const val EXTRA_ENCRYPTION_KEY = "encryption_key"
|
||||
|
||||
/** Intent extra key for custom title (optional). */
|
||||
const val EXTRA_CUSTOM_TITLE = "custom_title"
|
||||
|
||||
/** Intent extra key for custom subtitle (optional). */
|
||||
const val EXTRA_CUSTOM_SUBTITLE = "custom_subtitle"
|
||||
}
|
||||
|
||||
private lateinit var vaultStore: VaultStore
|
||||
|
||||
// UI components
|
||||
private lateinit var titleTextView: TextView
|
||||
private lateinit var subtitleTextView: TextView
|
||||
private lateinit var passwordEditText: EditText
|
||||
private lateinit var errorTextView: TextView
|
||||
private lateinit var errorContainer: View
|
||||
private lateinit var unlockButton: Button
|
||||
private lateinit var backButton: ImageButton
|
||||
private lateinit var loadingOverlay: View
|
||||
private lateinit var progressBar: ProgressBar
|
||||
|
||||
// State
|
||||
private var isProcessing: Boolean = false
|
||||
private var isShowingError: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_password_unlock)
|
||||
|
||||
// Keep screen on during password entry
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
// Adjust for soft keyboard - pan mode to ensure button stays visible
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
|
||||
|
||||
// Apply window insets for safe area
|
||||
applyWindowInsets()
|
||||
|
||||
// Initialize VaultStore
|
||||
vaultStore = VaultStore.getInstance(
|
||||
AndroidKeystoreProvider(this) { null },
|
||||
AndroidStorageProvider(this),
|
||||
)
|
||||
|
||||
// Get custom title/subtitle from intent
|
||||
val customTitle = intent.getStringExtra(EXTRA_CUSTOM_TITLE)
|
||||
val customSubtitle = intent.getStringExtra(EXTRA_CUSTOM_SUBTITLE)
|
||||
|
||||
// Initialize views
|
||||
initializeViews(customTitle, customSubtitle)
|
||||
}
|
||||
|
||||
private fun initializeViews(customTitle: String?, customSubtitle: String?) {
|
||||
titleTextView = findViewById(R.id.titleTextView)
|
||||
subtitleTextView = findViewById(R.id.subtitleTextView)
|
||||
passwordEditText = findViewById(R.id.passwordEditText)
|
||||
errorTextView = findViewById(R.id.errorTextView)
|
||||
errorContainer = findViewById(R.id.errorContainer)
|
||||
unlockButton = findViewById(R.id.unlockButton)
|
||||
backButton = findViewById(R.id.backButton)
|
||||
loadingOverlay = findViewById(R.id.loadingOverlay)
|
||||
progressBar = findViewById(R.id.progressBar)
|
||||
|
||||
// Set custom title/subtitle if provided
|
||||
if (!customTitle.isNullOrEmpty()) {
|
||||
titleTextView.text = customTitle
|
||||
} else {
|
||||
titleTextView.text = getString(R.string.password_unlock_title)
|
||||
}
|
||||
|
||||
if (!customSubtitle.isNullOrEmpty()) {
|
||||
subtitleTextView.text = customSubtitle
|
||||
} else {
|
||||
subtitleTextView.text = getString(R.string.password_unlock_subtitle)
|
||||
}
|
||||
|
||||
// Setup button states
|
||||
unlockButton.isEnabled = false
|
||||
|
||||
// Animate views in on appear
|
||||
animateViewsIn()
|
||||
|
||||
// Handle password input
|
||||
passwordEditText.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// Not used
|
||||
}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
// Not used
|
||||
}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
// Only hide error if user is typing (not when we programmatically clear for error display)
|
||||
if (!isShowingError) {
|
||||
hideError()
|
||||
}
|
||||
unlockButton.isEnabled = !s.isNullOrEmpty() && !isProcessing
|
||||
|
||||
// Animate button scale based on enabled state
|
||||
val scale = if (!s.isNullOrEmpty() && !isProcessing) 1.0f else 0.98f
|
||||
unlockButton.animate()
|
||||
.scaleX(scale)
|
||||
.scaleY(scale)
|
||||
.setDuration(200)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
})
|
||||
|
||||
passwordEditText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && unlockButton.isEnabled) {
|
||||
handleUnlock()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Setup button click listeners
|
||||
unlockButton.setOnClickListener {
|
||||
handleUnlock()
|
||||
}
|
||||
|
||||
backButton.setOnClickListener {
|
||||
setResult(RESULT_CANCELLED)
|
||||
finish()
|
||||
}
|
||||
|
||||
// Focus password field and show keyboard after a short delay
|
||||
passwordEditText.postDelayed({
|
||||
passwordEditText.requestFocus()
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(passwordEditText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
private fun animateViewsIn() {
|
||||
// Fade in and translate logo
|
||||
titleTextView.alpha = 0f
|
||||
titleTextView.translationY = -20f
|
||||
titleTextView.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(100)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
// Fade in subtitle
|
||||
subtitleTextView.alpha = 0f
|
||||
subtitleTextView.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(200)
|
||||
.start()
|
||||
|
||||
// Fade in and scale password field
|
||||
passwordEditText.alpha = 0f
|
||||
passwordEditText.scaleX = 0.95f
|
||||
passwordEditText.scaleY = 0.95f
|
||||
passwordEditText.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
// Fade in button
|
||||
unlockButton.alpha = 0f
|
||||
unlockButton.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(400)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun handleUnlock() {
|
||||
val password = passwordEditText.text.toString()
|
||||
if (password.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
isProcessing = true
|
||||
unlockButton.isEnabled = false
|
||||
passwordEditText.isEnabled = false
|
||||
loadingOverlay.visibility = View.VISIBLE
|
||||
hideError()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val encryptionKey = withContext(Dispatchers.IO) {
|
||||
vaultStore.verifyPassword(password)
|
||||
}
|
||||
|
||||
if (encryptionKey != null) {
|
||||
// Success - return encryption key
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(EXTRA_ENCRYPTION_KEY, encryptionKey)
|
||||
}
|
||||
setResult(RESULT_SUCCESS, resultIntent)
|
||||
finish()
|
||||
} else {
|
||||
// Incorrect password
|
||||
showError(getString(R.string.password_unlock_incorrect))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Error during verification
|
||||
android.util.Log.e("PasswordUnlockActivity", "Password verification failed", e)
|
||||
showError(getString(R.string.password_unlock_error))
|
||||
} finally {
|
||||
isProcessing = false
|
||||
passwordEditText.isEnabled = true
|
||||
loadingOverlay.visibility = View.GONE
|
||||
// Update button state based on password field content
|
||||
unlockButton.isEnabled = passwordEditText.text?.isNotEmpty() == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
errorTextView.text = message
|
||||
isShowingError = true
|
||||
|
||||
// Animate error in with slide from top
|
||||
errorContainer.visibility = View.VISIBLE
|
||||
errorContainer.alpha = 0f
|
||||
errorContainer.translationY = -20f
|
||||
errorContainer.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
// Shake password field to indicate error
|
||||
val shake = ObjectAnimator.ofFloat(passwordEditText, "translationX", 0f, 25f, -25f, 25f, -25f, 15f, -15f, 6f, -6f, 0f)
|
||||
shake.duration = 600
|
||||
shake.start()
|
||||
|
||||
passwordEditText.text?.clear()
|
||||
|
||||
// Reset flag after clearing is done
|
||||
passwordEditText.post {
|
||||
isShowingError = false
|
||||
}
|
||||
|
||||
passwordEditText.requestFocus()
|
||||
}
|
||||
|
||||
private fun hideError() {
|
||||
if (errorContainer.visibility == View.VISIBLE) {
|
||||
isShowingError = false
|
||||
errorContainer.animate()
|
||||
.alpha(0f)
|
||||
.translationY(-20f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
errorContainer.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyWindowInsets() {
|
||||
findViewById<View>(android.R.id.content).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemBarsInsets = insets.systemWindowInsets
|
||||
val backButtonParent = backButton.parent as View
|
||||
val layoutParams = backButtonParent.layoutParams as androidx.constraintlayout.widget.ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = systemBarsInsets.top + 8
|
||||
backButtonParent.layoutParams = layoutParams
|
||||
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (!isProcessing) {
|
||||
setResult(RESULT_CANCELLED)
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.os.Vibrator
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
@@ -133,15 +134,14 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
|
||||
private fun applyWindowInsets() {
|
||||
findViewById<View>(android.R.id.content).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val cancelButton = findViewById<Button>(R.id.cancelButton)
|
||||
val cancelButton = findViewById<ImageButton>(R.id.cancelButton)
|
||||
val systemBarsInsets = insets.systemWindowInsets
|
||||
|
||||
cancelButton.setPadding(
|
||||
cancelButton.paddingLeft,
|
||||
systemBarsInsets.top + cancelButton.paddingTop,
|
||||
cancelButton.paddingRight,
|
||||
cancelButton.paddingBottom,
|
||||
)
|
||||
// Apply top inset to cancel button's parent FrameLayout margin
|
||||
val cancelButtonParent = cancelButton.parent as View
|
||||
val layoutParams = cancelButtonParent.layoutParams as androidx.constraintlayout.widget.ConstraintLayout.LayoutParams
|
||||
layoutParams.topMargin = systemBarsInsets.top + 8 // 8dp base margin
|
||||
cancelButtonParent.layoutParams = layoutParams
|
||||
|
||||
insets
|
||||
}
|
||||
@@ -158,7 +158,7 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
progressBar = findViewById(R.id.progressBar)
|
||||
|
||||
// Cancel button
|
||||
findViewById<Button>(R.id.cancelButton).setOnClickListener {
|
||||
findViewById<ImageButton>(R.id.cancelButton).setOnClickListener {
|
||||
setResult(RESULT_CANCELLED)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -224,6 +224,42 @@ class VaultStore(
|
||||
return crypto.encryptDecryptionKeyForMobileLogin(publicKeyJWK, auth.getAuthMethods())
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password and return encryption key if correct.
|
||||
* Returns null if password is incorrect.
|
||||
*
|
||||
* @param password The password to verify
|
||||
* @return The base64-encoded encryption key if password is correct, null otherwise
|
||||
*/
|
||||
@Suppress("SwallowedException")
|
||||
fun verifyPassword(password: String): String? {
|
||||
return try {
|
||||
// Get encryption key derivation parameters
|
||||
val params = crypto.getEncryptionKeyDerivationParams()
|
||||
val paramsJson = org.json.JSONObject(params)
|
||||
val salt = paramsJson.getString("salt")
|
||||
val encryptionType = paramsJson.getString("encryptionType")
|
||||
val encryptionSettings = paramsJson.getString("encryptionSettings")
|
||||
|
||||
// Derive key from password
|
||||
val derivedKey = crypto.deriveKeyFromPassword(password, salt, encryptionType, encryptionSettings)
|
||||
|
||||
// Try to decrypt the vault to verify the password is correct
|
||||
val encryptedDb = databaseComponent.getEncryptedDatabase()
|
||||
val encryptedDbBytes = android.util.Base64.decode(encryptedDb, android.util.Base64.NO_WRAP)
|
||||
|
||||
// Attempt decryption to verify password is correct
|
||||
VaultCrypto.decrypt(encryptedDbBytes, derivedKey)
|
||||
|
||||
// If decryption succeeded, return the key as base64
|
||||
android.util.Base64.encodeToString(derivedKey, android.util.Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
// Password incorrect or decryption failed - intentionally return null
|
||||
// We don't log the error as this is expected when password is incorrect
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Database Methods
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#5c4331"
|
||||
android:endColor="?android:attr/colorBackground"
|
||||
android:type="linear" />
|
||||
</shape>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/primary"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/holo_red_dark"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8H8.9V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<gradient
|
||||
android:angle="270"
|
||||
android:startColor="#f6dfc4"
|
||||
android:endColor="?android:attr/colorBackground"
|
||||
android:type="linear" />
|
||||
</shape>
|
||||
@@ -0,0 +1,217 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:background="?android:attr/colorBackground">
|
||||
|
||||
<!-- Header Section -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headerSection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/password_unlock_gradient"
|
||||
android:paddingBottom="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<!-- Logo -->
|
||||
<ImageView
|
||||
android:id="@+id/logoImageView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:contentDescription="@string/aliasvault_icon"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="80dp" />
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/password_unlock_title"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintTop_toBottomOf="@id/logoImageView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp"
|
||||
tools:text="Unlock Vault" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/subtitleTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/password_unlock_subtitle"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleTextView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:paddingBottom="24dp"
|
||||
tools:text="Enter your master password" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Main Content Section -->
|
||||
<ScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerSection"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="32dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
||||
<!-- Password Input -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/passwordInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password_unlock_password_hint"
|
||||
app:boxStrokeColor="@color/primary"
|
||||
app:boxStrokeWidth="2dp"
|
||||
app:hintTextColor="?android:attr/textColorSecondary"
|
||||
app:startIconDrawable="@drawable/ic_lock"
|
||||
app:startIconTint="?android:attr/textColorSecondary"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusBottomEnd="12dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/passwordEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
android:padding="16dp" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Error Message with icon -->
|
||||
<LinearLayout
|
||||
android:id="@+id/errorContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/passwordInputLayout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:src="@drawable/ic_error"
|
||||
android:contentDescription="Error icon"
|
||||
android:tint="@android:color/holo_red_dark"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
tools:text="Incorrect password" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Unlock Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/unlockButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="54dp"
|
||||
android:text="@string/password_unlock_button"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="17sp"
|
||||
android:textStyle="bold"
|
||||
android:backgroundTint="@color/primary"
|
||||
app:icon="@drawable/ic_arrow_forward"
|
||||
app:iconGravity="end"
|
||||
app:iconTint="@android:color/white"
|
||||
app:cornerRadius="12dp"
|
||||
app:elevation="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/errorContainer"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- Back Button -->
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
android:contentDescription="@string/common_back"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
android:padding="12dp"
|
||||
android:elevation="8dp" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<FrameLayout
|
||||
android:id="@+id/loadingOverlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#80000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateTint="@color/primary" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -7,65 +7,61 @@
|
||||
android:background="?android:attr/colorBackground"
|
||||
tools:context=".pinunlock.PinUnlockActivity">
|
||||
|
||||
<!-- Header with Cancel Button -->
|
||||
<Button
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="wrap_content"
|
||||
<!-- Header Section with Gradient Background -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/headerSection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/common_cancel"
|
||||
android:textColor="@color/primary"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:minWidth="64dp"
|
||||
android:background="@drawable/password_unlock_gradient"
|
||||
android:paddingBottom="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- Logo -->
|
||||
<ImageView
|
||||
android:id="@+id/logoImageView"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:contentDescription="@string/aliasvault_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/cancelButton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="16dp" />
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_unlock_vault"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintTop_toBottomOf="@id/logoImageView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="12dp" />
|
||||
<!-- Logo -->
|
||||
<ImageView
|
||||
android:id="@+id/logoImageView"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:contentDescription="@string/aliasvault_icon"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="80dp" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/subtitleTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_enter_to_unlock"
|
||||
android:textSize="15sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleTextView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp" />
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/titleTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_unlock_vault"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintTop_toBottomOf="@id/logoImageView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="12dp" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/subtitleTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_enter_to_unlock"
|
||||
android:textSize="15sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleTextView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:paddingBottom="24dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- PIN Dots Container (for fixed-length PINs) -->
|
||||
<LinearLayout
|
||||
@@ -74,10 +70,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/subtitleTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerSection"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:visibility="visible" />
|
||||
|
||||
<!-- PIN Text View (for variable-length PINs) -->
|
||||
@@ -91,10 +87,10 @@
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:letterSpacing="0.2"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/subtitleTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/headerSection"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Error Message -->
|
||||
@@ -266,4 +262,25 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<!-- Back Button - Floating over content -->
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancelButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_arrow_back"
|
||||
android:contentDescription="@string/common_cancel"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:tint="?android:attr/textColorPrimary"
|
||||
android:padding="12dp"
|
||||
android:elevation="8dp" />
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="common_close">Close</string>
|
||||
<string name="common_next">Next</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_back">Back</string>
|
||||
<string name="unknown_error">An unknown error occurred</string>
|
||||
|
||||
<!-- AutofillService strings -->
|
||||
@@ -91,4 +92,12 @@
|
||||
<string name="pin_confirm_title">Confirm PIN</string>
|
||||
<string name="pin_confirm_description">Re-enter your PIN to confirm</string>
|
||||
<string name="pin_mismatch">PINs do not match. Please try again.</string>
|
||||
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Unlock Vault</string>
|
||||
<string name="password_unlock_subtitle">Enter your master password</string>
|
||||
<string name="password_unlock_password_hint">Password</string>
|
||||
<string name="password_unlock_button">Unlock</string>
|
||||
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user