Merge branch 'main' into 773-feature-request-add-encrypted-aliasvault-vault-export-and-import
* main: (26 commits) Prevent keyboard shortcuts from firing when interacting with autofill popup (#1832) Auto-disable email alias when item is deleted in browser extension (#1830) Add local password unlock rate limit to browser extension (#1824) Add local password unlock rate limit to iOS and Android apps (#1824) Fix password unlock error disappearing too quickly on Android (#1824) Add release notes for 0.27.2 New translations en.json (Danish) Update translations from Crowdin [ci skip] Update FormDetector.ts (#1821) Update Android boot splash icon to be transparent (#1819) Tweak settings UI so both label and value will wrap text if necessary (#1819) Tweak unlock screen button sizing to work with longer translations (#1819) Update FormDetector.ts Update print-latest-changelogs.sh Bump version to 0.28.0-alpha Add release notes for 0.27.1 New Crowdin updates (#1781) Update DatabaseMessageStore.cs Bump the nuget group with 1 update Update linting (#1812) Update confirm dialog to show text in center when it wraps due to translations (#1812) ...
@@ -1 +1 @@
|
||||
27
|
||||
28
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.27.0-alpha
|
||||
0.28.0-alpha
|
||||
|
||||
11
apps/browser-extension/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"dompurify": "^3.3.2",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
@@ -4993,10 +4993,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aliasvault-browser-extension",
|
||||
"description": "AliasVault Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.27.0",
|
||||
"version": "0.28.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:rust": "cd ../../core/rust && ./build.sh --browser",
|
||||
@@ -36,7 +36,7 @@
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"buffer": "^6.0.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"dompurify": "^3.3.2",
|
||||
"globals": "^16.0.0",
|
||||
"i18next": "^25.3.1",
|
||||
"otpauth": "^9.3.6",
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2700102;
|
||||
CURRENT_PROJECT_VERSION = 2800100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -476,7 +476,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.27.0;
|
||||
MARKETING_VERSION = 0.28.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -495,7 +495,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2700102;
|
||||
CURRENT_PROJECT_VERSION = 2800100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -508,7 +508,7 @@
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.27.0;
|
||||
MARKETING_VERSION = 0.28.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -532,7 +532,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2700102;
|
||||
CURRENT_PROJECT_VERSION = 2800100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -547,7 +547,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.27.0;
|
||||
MARKETING_VERSION = 0.28.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
@@ -571,7 +571,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 2700102;
|
||||
CURRENT_PROJECT_VERSION = 2800100;
|
||||
DEVELOPMENT_TEAM = 8PHW4HN3F7;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -586,7 +586,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 0.27.0;
|
||||
MARKETING_VERSION = 0.28.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"-framework",
|
||||
SafariServices,
|
||||
|
||||
@@ -293,6 +293,9 @@ export async function handleClearSession(): Promise<messageBoolResponse> {
|
||||
'session:navigationHistory',
|
||||
]);
|
||||
|
||||
// Reset password unlock failed attempts counter on logout
|
||||
await LocalPreferencesService.resetPasswordUnlockFailedAttempts();
|
||||
|
||||
// Clear cached client since session ended
|
||||
cachedSqliteClient = null;
|
||||
cachedVaultBlob = null;
|
||||
|
||||
@@ -208,6 +208,21 @@ async function checkAndRestoreSavePromptEarly(ctx: Parameters<typeof createShado
|
||||
* Mount handler for early save prompt restore.
|
||||
*/
|
||||
onMount(container) {
|
||||
/**
|
||||
* Stop keyboard event propagation to prevent host page shortcuts from triggering
|
||||
* when typing in save prompt input fields.
|
||||
*/
|
||||
const handleKeyboardEvent = (e: KeyboardEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target && container.contains(target)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener('keydown', handleKeyboardEvent, true);
|
||||
container.addEventListener('keyup', handleKeyboardEvent, true);
|
||||
container.addEventListener('keypress', handleKeyboardEvent, true);
|
||||
|
||||
// Restore the appropriate prompt type based on persisted state
|
||||
if (persistedState.promptType === 'add-url') {
|
||||
void restoreAddUrlPromptFromState(
|
||||
@@ -434,6 +449,23 @@ export default defineContentScript({
|
||||
* Handle mount.
|
||||
*/
|
||||
onMount(container) {
|
||||
/**
|
||||
* Stop keyboard event propagation to prevent host page shortcuts from triggering
|
||||
* when typing in extension popups (e.g., Discourse "u" shortcut for "go back").
|
||||
*/
|
||||
const handleKeyboardEvent = (e: KeyboardEvent): void => {
|
||||
// Only stop propagation if the event originated from within our shadow DOM
|
||||
const target = e.target as HTMLElement;
|
||||
if (target && container.contains(target)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Capture keyboard events at the container level to prevent bubbling to host page
|
||||
container.addEventListener('keydown', handleKeyboardEvent, true);
|
||||
container.addEventListener('keyup', handleKeyboardEvent, true);
|
||||
container.addEventListener('keypress', handleKeyboardEvent, true);
|
||||
|
||||
/**
|
||||
* Handle input field focus.
|
||||
*/
|
||||
|
||||
@@ -61,6 +61,11 @@ const Unlock: React.FC = () => {
|
||||
// Password unlock state
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [passwordFailedAttempts, setPasswordFailedAttempts] = useState(0);
|
||||
|
||||
// Brute force protection constants
|
||||
const MAX_PASSWORD_ATTEMPTS = 10;
|
||||
const PASSWORD_WARNING_THRESHOLD = 5;
|
||||
|
||||
// PIN unlock state
|
||||
const [pin, setPin] = useState('');
|
||||
@@ -138,6 +143,10 @@ const Unlock: React.FC = () => {
|
||||
setUnlockMode('password');
|
||||
}
|
||||
|
||||
// Load password failed attempts counter
|
||||
const storedAttempts = await LocalPreferencesService.getPasswordUnlockFailedAttempts();
|
||||
setPasswordFailedAttempts(storedAttempts);
|
||||
|
||||
// Then check API status
|
||||
await checkStatus();
|
||||
};
|
||||
@@ -294,8 +303,10 @@ const Unlock: React.FC = () => {
|
||||
// Clear dismiss until
|
||||
await LocalPreferencesService.setVaultLockedDismissUntil(0);
|
||||
|
||||
// Reset PIN failed attempts on successful password unlock
|
||||
// Reset PIN and password failed attempts on successful unlock
|
||||
await resetFailedAttempts();
|
||||
await LocalPreferencesService.resetPasswordUnlockFailedAttempts();
|
||||
setPasswordFailedAttempts(0);
|
||||
|
||||
// Navigate to reinitialize which will call syncVault to sync with server
|
||||
navigate('/reinitialize', { replace: true });
|
||||
@@ -307,13 +318,13 @@ const Unlock: React.FC = () => {
|
||||
// Check if it's a decryption failure (E-203): this means wrong password
|
||||
const errorCode = extractErrorCode(getErrorMessage(err, ''));
|
||||
if (errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) {
|
||||
setError(t('common.errors.wrongPassword'));
|
||||
await handlePasswordFailedAttempt();
|
||||
} else {
|
||||
// Other error codes, show the formatted message as-is
|
||||
setError(getErrorMessage(err, t('common.errors.wrongPassword')));
|
||||
}
|
||||
} else {
|
||||
setError(t('common.errors.wrongPassword'));
|
||||
await handlePasswordFailedAttempt();
|
||||
}
|
||||
console.error('Unlock error:', err);
|
||||
} finally {
|
||||
@@ -445,6 +456,35 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle failed password attempt with brute force protection
|
||||
*/
|
||||
const handlePasswordFailedAttempt = async (): Promise<void> => {
|
||||
const newAttempts = passwordFailedAttempts + 1;
|
||||
setPasswordFailedAttempts(newAttempts);
|
||||
await LocalPreferencesService.setPasswordUnlockFailedAttempts(newAttempts);
|
||||
|
||||
const remainingAttempts = MAX_PASSWORD_ATTEMPTS - newAttempts;
|
||||
|
||||
// Clear password field
|
||||
setPassword('');
|
||||
|
||||
if (newAttempts >= MAX_PASSWORD_ATTEMPTS) {
|
||||
// Max attempts reached - logout user
|
||||
setError(t('auth.maxAttemptsReached'));
|
||||
// Delay to let user read the message
|
||||
setTimeout(async () => {
|
||||
await authContext.clearAuthUserInitiated();
|
||||
}, 2000);
|
||||
} else if (newAttempts >= PASSWORD_WARNING_THRESHOLD) {
|
||||
// Show warning about remaining attempts
|
||||
setError(t('auth.passwordAttemptsWarning', { remainingAttempts }));
|
||||
} else {
|
||||
// Show standard incorrect password error
|
||||
setError(t('common.errors.wrongPassword'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle logout click - opens the logout confirmation modal.
|
||||
*/
|
||||
@@ -514,8 +554,10 @@ const Unlock: React.FC = () => {
|
||||
// Clear dismiss until
|
||||
await LocalPreferencesService.setVaultLockedDismissUntil(0);
|
||||
|
||||
// Reset PIN failed attempts on successful unlock
|
||||
// Reset PIN and password failed attempts on successful unlock
|
||||
await resetFailedAttempts();
|
||||
await LocalPreferencesService.resetPasswordUnlockFailedAttempts();
|
||||
setPasswordFailedAttempts(0);
|
||||
|
||||
// Navigate to reinitialize which will call syncVault to sync with server
|
||||
navigate('/reinitialize', { replace: true });
|
||||
@@ -671,7 +713,7 @@ const Unlock: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4 text-center" />}
|
||||
{error && <AlertMessage type="error" message={error} className="mb-4" />}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 dark:text-gray-200 font-medium mb-2" htmlFor="password">
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Затвори",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "Undo",
|
||||
"save": "Desa",
|
||||
"saving": "Saving...",
|
||||
"edit": "Edit",
|
||||
"edit": "Edita",
|
||||
"create": "Create",
|
||||
"or": "Or",
|
||||
"close": "Tanca",
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Tanca",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Aktualisierung des Tresors erforderlich.",
|
||||
"dismissPopup": "Popup schliessen",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Schließen",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
"switchAccounts": "Switch accounts?",
|
||||
"loginWithMobile": "Log in using Mobile App",
|
||||
"unlockWithMobile": "Unlock using Mobile App",
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault."
|
||||
"scanQrCode": "Scan this QR code with your AliasVault mobile app to log in and unlock your vault.",
|
||||
"passwordAttemptsWarning": "Incorrect password. You will be logged out if you enter the wrong password {{remainingAttempts}} more times.",
|
||||
"maxAttemptsReached": "Too many failed unlock attempts. You have been logged out for security reasons."
|
||||
},
|
||||
"menu": {
|
||||
"vault": "Vault",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "Kumoa",
|
||||
"save": "Tallenna",
|
||||
"saving": "Saving...",
|
||||
"edit": "Edit",
|
||||
"edit": "Muokkaa",
|
||||
"create": "Luo",
|
||||
"or": "Tai",
|
||||
"close": "Sulje",
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Holvin päivitys vaaditaan.",
|
||||
"dismissPopup": "Hylkää ponnahdusikkuna",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Sulje",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -141,11 +141,11 @@
|
||||
},
|
||||
"content": {
|
||||
"or": "ou",
|
||||
"new": "Nouveautés",
|
||||
"new": "Nouveau",
|
||||
"vaultLocked": "AliasVault est verrouillé.",
|
||||
"creatingNewAlias": "Création de nouveaux alias...",
|
||||
"noMatchesFound": "Aucun résultat trouvé",
|
||||
"searchVault": "Rechercher dans le coffre...",
|
||||
"searchVault": "Rechercher élément...",
|
||||
"enterServiceName": "Entrez le nom du service",
|
||||
"enterEmailAddress": "Entrer l'adresse email",
|
||||
"enterUsername": "Entrez le nom d'utilisateur",
|
||||
@@ -266,7 +266,7 @@
|
||||
"textArea": "Zone de texte"
|
||||
},
|
||||
"login": {
|
||||
"title": "Se connecter"
|
||||
"title": "Identifiant"
|
||||
},
|
||||
"alias": {
|
||||
"title": "Alias"
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "הסגה",
|
||||
"save": "שמירה",
|
||||
"saving": "Saving...",
|
||||
"edit": "Edit",
|
||||
"edit": "עריכה",
|
||||
"create": "יצירה",
|
||||
"or": "או",
|
||||
"close": "סגירה",
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "יש לשדרג את הכספת.",
|
||||
"dismissPopup": "התעלמות מחלונית",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "סגירה",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
"title": "Opslaan in AliasVault?",
|
||||
"neverForThisSite": "Nooit voor deze site",
|
||||
"addUrlTitle": "URL naar inloggegevens toevoegen?",
|
||||
"addUrl": "Url toevoegen"
|
||||
"addUrl": "URL toevoegen"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "Отменить",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохраняем...",
|
||||
"edit": "Edit",
|
||||
"edit": "Редактировать",
|
||||
"create": "Создать",
|
||||
"or": "Или",
|
||||
"close": "Закрыть",
|
||||
@@ -63,8 +63,8 @@
|
||||
"disabled": "Отключено",
|
||||
"showPassword": "Показать пароль",
|
||||
"hidePassword": "Скрыть пароль",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
"show": "Показать",
|
||||
"hide": "Скрыть",
|
||||
"showDetails": "Показать подробности",
|
||||
"hideDetails": "Скрыть подробности",
|
||||
"copyToClipboard": "Скопировать в буфер обмена",
|
||||
@@ -171,13 +171,13 @@
|
||||
"openAliasVaultToUpgrade": "Откройте AliasVault для обновления",
|
||||
"vaultUpgradeRequired": "Требуется обновление хранилища.",
|
||||
"dismissPopup": "Закрыть окно",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"noTotpItemsFound": "2FA-коды не найдены",
|
||||
"close": "Закрыть",
|
||||
"savePrompt": {
|
||||
"title": "Сохранить в AliasVault?",
|
||||
"neverForThisSite": "Никогда для этого сайта",
|
||||
"addUrlTitle": "Add URL to credential?",
|
||||
"addUrl": "Add URL"
|
||||
"addUrlTitle": "Добавить URL к этой записи?",
|
||||
"addUrl": "Добавить URL"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
@@ -215,7 +215,7 @@
|
||||
"filters": {
|
||||
"folders": "Папки",
|
||||
"passkeys": "Ключи доступа",
|
||||
"totp": "2FA Codes"
|
||||
"totp": "2FA-коды"
|
||||
},
|
||||
"sort": {
|
||||
"title": "Сортировка",
|
||||
@@ -302,8 +302,8 @@
|
||||
"secretKey": "Секретный ключ",
|
||||
"saveToViewCode": "Сохранить для просмотра кода",
|
||||
"defaultName": "Аутентификатор",
|
||||
"deleteTotpCodeTitle": "Delete 2FA Code",
|
||||
"deleteTotpCodeConfirmation": "Are you sure you want to delete the 2FA code \"{{name}}\"?",
|
||||
"deleteTotpCodeTitle": "Удаление 2FA-кода",
|
||||
"deleteTotpCodeConfirmation": "Вы уверены, что хотите удалить 2FA-код для \"{{name}}\"?",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Неверный формат секретного ключа."
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
"secretKey": "Hemlig nyckel",
|
||||
"saveToViewCode": "Spara för att visa kod",
|
||||
"defaultName": "Autentiserare",
|
||||
"deleteTotpCodeTitle": "Ta bort 2FA-kod",
|
||||
"deleteTotpCodeTitle": "Radera 2FA-kod",
|
||||
"deleteTotpCodeConfirmation": "Är du säker på att du vill radera 2FA-koden\"{{name}}\"?",
|
||||
"errors": {
|
||||
"invalidSecretKey": "Ogiltigt format för hemlig nyckel."
|
||||
@@ -469,7 +469,7 @@
|
||||
"helpText": "Passkeys skapas på webbplatsen när du blir tillfrågad. De kan inte redigeras manuellt. För att radera denna passkey, kan du ta bort det från denna posten. För att byta ut denna passkey eller skapa en ny, besök webbplatsen och följ dess anvisningar.",
|
||||
"passkeyMarkedForDeletion": "Passkey markerad för borttagning",
|
||||
"passkeyWillBeDeleted": "Denna passkey kommer att tas bort när du sparar dessa uppgifter.",
|
||||
"useBrowserPasskey": "Använd Webbläsar Passkey",
|
||||
"useBrowserPasskey": "Använd webbläsarens Passkey",
|
||||
"bypass": {
|
||||
"description": "Hur länge vill du använda webbläsarens passkey-leverantör till {{origin}}?",
|
||||
"thisTimeOnly": "Endast denna gång",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "Undo",
|
||||
"save": "Kaydet",
|
||||
"saving": "Saving...",
|
||||
"edit": "Edit",
|
||||
"edit": "Düzenle",
|
||||
"create": "Create",
|
||||
"or": "Or",
|
||||
"close": "Kapat",
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Kapat",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Потрібне оновлення сховища.",
|
||||
"dismissPopup": "Закрити спливаюче вікно",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "Закрити",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"undo": "Undo",
|
||||
"save": "محفوظ کریں",
|
||||
"saving": "Saving...",
|
||||
"edit": "Edit",
|
||||
"edit": "ترمیم کریں",
|
||||
"create": "Create",
|
||||
"or": "Or",
|
||||
"close": "بند کریں",
|
||||
@@ -172,7 +172,7 @@
|
||||
"vaultUpgradeRequired": "Vault upgrade required.",
|
||||
"dismissPopup": "Dismiss popup",
|
||||
"noTotpItemsFound": "No 2FA code matches found",
|
||||
"close": "Close",
|
||||
"close": "بند کریں",
|
||||
"savePrompt": {
|
||||
"title": "Save to AliasVault?",
|
||||
"neverForThisSite": "Never for this site",
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AppInfo {
|
||||
/**
|
||||
* The current extension version. This should be updated with each release of the extension.
|
||||
*/
|
||||
public static readonly VERSION = '0.27.0-alpha';
|
||||
public static readonly VERSION = '0.28.0-alpha';
|
||||
|
||||
/**
|
||||
* The API version to send to the server (base semver without stage suffixes).
|
||||
|
||||
@@ -39,6 +39,9 @@ const KEYS = {
|
||||
// Session/Navigation state
|
||||
PENDING_REDIRECT_URL: 'session:pendingRedirectUrl',
|
||||
SKIP_FORM_RESTORE: 'local:aliasvault_skip_form_restore',
|
||||
|
||||
// Brute force protection
|
||||
PASSWORD_UNLOCK_FAILED_ATTEMPTS: 'local:password_unlock_failed_attempts',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -407,4 +410,39 @@ export const LocalPreferencesService = {
|
||||
async setLoginSaveBlockedDomains(domains: string[]): Promise<void> {
|
||||
await storage.setItem(KEYS.LOGIN_SAVE_BLOCKED_DOMAINS, domains);
|
||||
},
|
||||
|
||||
/*
|
||||
* ============================================
|
||||
* Brute Force Protection
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the password unlock failed attempts count.
|
||||
* @returns The number of failed password unlock attempts. Defaults to 0.
|
||||
*/
|
||||
async getPasswordUnlockFailedAttempts(): Promise<number> {
|
||||
const value = await storage.getItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS) as number | string | null;
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return parseInt(value, 10) || 0;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the password unlock failed attempts count.
|
||||
*/
|
||||
async setPasswordUnlockFailedAttempts(attempts: number): Promise<void> {
|
||||
await storage.setItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS, attempts);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the password unlock failed attempts counter.
|
||||
*/
|
||||
async resetPasswordUnlockFailedAttempts(): Promise<void> {
|
||||
await storage.removeItem(KEYS.PASSWORD_UNLOCK_FAILED_ATTEMPTS);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -226,7 +226,8 @@ export class ItemQueries {
|
||||
AND fv.Value IS NOT NULL
|
||||
AND fv.Value != ''
|
||||
AND fv.IsDeleted = 0
|
||||
AND i.IsDeleted = 0`;
|
||||
AND i.IsDeleted = 0
|
||||
AND i.DeletedAt IS NULL`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,8 +53,14 @@ export class FormDetector {
|
||||
let formWrapper = this.getFormWrapper();
|
||||
|
||||
if (formWrapper?.getAttribute('role') === 'dialog') {
|
||||
// If we hit a dialog, search for form only within the dialog
|
||||
formWrapper = formWrapper.querySelector('form') as HTMLElement | null ?? formWrapper;
|
||||
/*
|
||||
* If we hit a dialog, try to find a more specific container within it.
|
||||
* Try in order: <form>, custom form elements (like faceplate-form), or keep the dialog.
|
||||
*/
|
||||
const standardForm = formWrapper.querySelector('form') as HTMLElement | null;
|
||||
const customFormElement = formWrapper.querySelector('[id*="login"], [id*="register"], [class*="auth"], [class*="login"], [class*="register"]') as HTMLElement | null;
|
||||
|
||||
formWrapper = standardForm ?? customFormElement ?? formWrapper;
|
||||
}
|
||||
|
||||
if (!formWrapper) {
|
||||
@@ -399,28 +405,30 @@ export class FormDetector {
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if element has zero or near-zero dimensions (effectively invisible)
|
||||
* This catches various hiding techniques:
|
||||
* - height:0, width:0, max-height:0, max-width:0
|
||||
* - position:absolute with clip/clip-path
|
||||
* - Any combination that results in no visible pixels
|
||||
* Check if element has zero dimensions using actual rendered size.
|
||||
* Only check this for input elements themselves, not their parent containers.
|
||||
* Container elements (divs, fieldsets, etc.) may have zero dimensions but contain visible children.
|
||||
* This check is primarily to catch fake/honeypot input fields.
|
||||
*/
|
||||
const height = parseFloat(style.height);
|
||||
const width = parseFloat(style.width);
|
||||
const maxHeight = parseFloat(style.maxHeight);
|
||||
const maxWidth = parseFloat(style.maxWidth);
|
||||
const isInputElement = current.tagName.toLowerCase() === 'input' ||
|
||||
current.tagName.toLowerCase() === 'textarea' ||
|
||||
current.tagName.toLowerCase() === 'select';
|
||||
|
||||
// Check if element has zero dimensions
|
||||
if (height === 0 || width === 0 || maxHeight === 0 || maxWidth === 0) {
|
||||
// Cache and return false for this element and all its parents
|
||||
let parent: HTMLElement | null = current;
|
||||
while (parent) {
|
||||
if (isInputElement) {
|
||||
const rect = current.getBoundingClientRect();
|
||||
const height = parseFloat(style.height);
|
||||
const width = parseFloat(style.width);
|
||||
const maxHeight = parseFloat(style.maxHeight);
|
||||
const maxWidth = parseFloat(style.maxWidth);
|
||||
|
||||
// Only reject if both bounding rect is 0x0 AND has explicit zero-sizing styles
|
||||
if (rect.width === 0 && rect.height === 0 &&
|
||||
(height === 0 || width === 0 || maxHeight === 0 || maxWidth === 0)) {
|
||||
if (checkOpacity) {
|
||||
this.visibilityCache.set(parent, false);
|
||||
this.visibilityCache.set(current, false);
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineConfig({
|
||||
return {
|
||||
name: "AliasVault",
|
||||
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
|
||||
version: "0.27.0",
|
||||
version: "0.28.0",
|
||||
content_security_policy: {
|
||||
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
|
||||
},
|
||||
|
||||
@@ -93,8 +93,8 @@ android {
|
||||
applicationId 'net.aliasvault.app'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2700102
|
||||
versionName "0.27.0-alpha"
|
||||
versionCode 2800100
|
||||
versionName "0.28.0-alpha"
|
||||
|
||||
// Instrumented test configuration
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -291,6 +291,11 @@ class MainActivity : ReactActivity() {
|
||||
// For authenticateUser(), resolve with false
|
||||
authPromise?.resolve(false)
|
||||
}
|
||||
net.aliasvault.app.passwordunlock.PasswordUnlockActivity.RESULT_MAX_ATTEMPTS_REACHED -> {
|
||||
// Max attempts reached - vault has been cleared, reject to trigger logout in React Native
|
||||
passwordPromise?.reject("MAX_ATTEMPTS_REACHED", "Too many failed unlock attempts", null)
|
||||
authPromise?.reject("MAX_ATTEMPTS_REACHED", "Too many failed unlock attempts", null)
|
||||
}
|
||||
else -> {
|
||||
// For showPasswordUnlock(), resolve with null
|
||||
passwordPromise?.resolve(null)
|
||||
|
||||
@@ -130,6 +130,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
override fun clearSession(promise: Promise) {
|
||||
try {
|
||||
vaultStore.clearSession()
|
||||
|
||||
// Reset password unlock failed attempts counter on logout
|
||||
val sharedPreferences = reactApplicationContext.getSharedPreferences("aliasvault", android.content.Context.MODE_PRIVATE)
|
||||
sharedPreferences.edit().remove("password_unlock_failed_attempts").apply()
|
||||
|
||||
promise.resolve(null)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error clearing session", e)
|
||||
@@ -1346,6 +1351,24 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if biometric unlock is actually available (device + key validation).
|
||||
* This checks not only if biometrics are configured in auth methods,
|
||||
* but also validates that the encryption key in KeyStore is valid.
|
||||
* Returns false if key has been invalidated (e.g., biometric enrollment changed).
|
||||
* @param promise The promise to resolve with boolean result.
|
||||
*/
|
||||
@ReactMethod
|
||||
override fun isBiometricUnlockAvailable(promise: Promise) {
|
||||
try {
|
||||
val available = vaultStore.isBiometricAuthEnabled()
|
||||
promise.resolve(available)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking biometric unlock availability", e)
|
||||
promise.reject("ERR_BIOMETRIC_CHECK", "Failed to check biometric unlock availability: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show native PIN setup UI.
|
||||
* Launches the native PinUnlockActivity in setup mode.
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
@@ -19,6 +20,7 @@ import android.widget.ImageButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -45,6 +47,9 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
/** Result code for cancelled password unlock. */
|
||||
const val RESULT_CANCELLED = Activity.RESULT_CANCELED
|
||||
|
||||
/** Result code for max attempts reached - user has been logged out. */
|
||||
const val RESULT_MAX_ATTEMPTS_REACHED = Activity.RESULT_FIRST_USER + 1
|
||||
|
||||
/** Intent extra key for the encryption key (returned on success). */
|
||||
const val EXTRA_ENCRYPTION_KEY = "encryption_key"
|
||||
|
||||
@@ -56,6 +61,15 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
|
||||
/** Intent extra key for custom button text (optional). */
|
||||
const val EXTRA_CUSTOM_BUTTON_TEXT = "custom_button_text"
|
||||
|
||||
/** Maximum number of failed password attempts before logout. */
|
||||
private const val MAX_FAILED_ATTEMPTS = 10
|
||||
|
||||
/** Warning threshold for failed attempts. */
|
||||
private const val WARNING_THRESHOLD = 5
|
||||
|
||||
/** SharedPreferences key for failed password attempts counter. */
|
||||
private const val PREF_FAILED_ATTEMPTS = "password_unlock_failed_attempts"
|
||||
}
|
||||
|
||||
private lateinit var vaultStore: VaultStore
|
||||
@@ -74,6 +88,7 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
// State
|
||||
private var isProcessing: Boolean = false
|
||||
private var isShowingError: Boolean = false
|
||||
private var failedAttempts: Int = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -94,6 +109,10 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
AndroidStorageProvider(this),
|
||||
)
|
||||
|
||||
// Load failed attempts counter
|
||||
val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE)
|
||||
failedAttempts = sharedPreferences.getInt(PREF_FAILED_ATTEMPTS, 0)
|
||||
|
||||
// Get custom title/subtitle/buttonText from intent
|
||||
val customTitle = intent.getStringExtra(EXTRA_CUSTOM_TITLE)
|
||||
val customSubtitle = intent.getStringExtra(EXTRA_CUSTOM_SUBTITLE)
|
||||
@@ -148,8 +167,8 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
// Not used
|
||||
}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
// Only hide error if user is typing (not when we programmatically clear for error display)
|
||||
if (!isShowingError) {
|
||||
// Hide error when user starts typing a new password
|
||||
if (!isShowingError && !s.isNullOrEmpty()) {
|
||||
hideError()
|
||||
}
|
||||
unlockButton.isEnabled = !s.isNullOrEmpty() && !isProcessing
|
||||
@@ -253,15 +272,16 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (encryptionKey != null) {
|
||||
// Success - return encryption key
|
||||
// Success - reset failed attempts counter and return encryption key
|
||||
resetFailedAttempts()
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(EXTRA_ENCRYPTION_KEY, encryptionKey)
|
||||
}
|
||||
setResult(RESULT_SUCCESS, resultIntent)
|
||||
finish()
|
||||
} else {
|
||||
// Incorrect password
|
||||
showError(getString(R.string.password_unlock_incorrect))
|
||||
// Incorrect password - increment failed attempts
|
||||
handleFailedAttempt()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Error during verification
|
||||
@@ -278,9 +298,11 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
errorTextView.text = message
|
||||
errorContainer.animate().cancel()
|
||||
isShowingError = true
|
||||
|
||||
errorTextView.text = message
|
||||
|
||||
// Animate error in with slide from top
|
||||
errorContainer.visibility = View.VISIBLE
|
||||
errorContainer.alpha = 0f
|
||||
@@ -290,6 +312,7 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
.translationY(0f)
|
||||
.setDuration(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.setListener(null) // Clear any previous listeners
|
||||
.start()
|
||||
|
||||
// Shake password field to indicate error
|
||||
@@ -298,18 +321,15 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
shake.start()
|
||||
|
||||
passwordEditText.text?.clear()
|
||||
|
||||
// Reset flag after clearing is done
|
||||
passwordEditText.post {
|
||||
passwordEditText.postDelayed({
|
||||
isShowingError = false
|
||||
}
|
||||
}, 100)
|
||||
|
||||
passwordEditText.requestFocus()
|
||||
}
|
||||
|
||||
private fun hideError() {
|
||||
if (errorContainer.visibility == View.VISIBLE) {
|
||||
isShowingError = false
|
||||
if (errorContainer.visibility == View.VISIBLE && !isShowingError) {
|
||||
errorContainer.animate()
|
||||
.alpha(0f)
|
||||
.translationY(-20f)
|
||||
@@ -324,6 +344,64 @@ class PasswordUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFailedAttempt() {
|
||||
failedAttempts++
|
||||
saveFailedAttempts()
|
||||
|
||||
val remainingAttempts = MAX_FAILED_ATTEMPTS - failedAttempts
|
||||
|
||||
if (failedAttempts >= MAX_FAILED_ATTEMPTS) {
|
||||
// Max attempts reached - logout user
|
||||
logoutUser()
|
||||
} else if (failedAttempts >= WARNING_THRESHOLD) {
|
||||
// Show warning about remaining attempts
|
||||
val warningMessage = getString(R.string.password_unlock_attempts_warning, remainingAttempts)
|
||||
showError(warningMessage)
|
||||
} else {
|
||||
// Show standard incorrect password error
|
||||
showError(getString(R.string.password_unlock_incorrect))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFailedAttempts() {
|
||||
val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE)
|
||||
sharedPreferences.edit {
|
||||
putInt(PREF_FAILED_ATTEMPTS, failedAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetFailedAttempts() {
|
||||
failedAttempts = 0
|
||||
val sharedPreferences = getSharedPreferences("aliasvault", Context.MODE_PRIVATE)
|
||||
sharedPreferences.edit {
|
||||
remove(PREF_FAILED_ATTEMPTS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logoutUser() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
// Clear vault and all session data
|
||||
withContext(Dispatchers.IO) {
|
||||
vaultStore.clearVault()
|
||||
}
|
||||
|
||||
// Show logout message and close activity
|
||||
showError(getString(R.string.password_unlock_max_attempts_reached))
|
||||
|
||||
// Delay to let user read the message, then return max attempts result
|
||||
passwordEditText.postDelayed({
|
||||
setResult(RESULT_MAX_ATTEMPTS_REACHED)
|
||||
finish()
|
||||
}, 2000)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("PasswordUnlockActivity", "Error during logout", e)
|
||||
setResult(RESULT_MAX_ATTEMPTS_REACHED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyWindowInsets() {
|
||||
findViewById<View>(android.R.id.content).setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemBarsInsets = insets.systemWindowInsets
|
||||
|
||||
@@ -128,7 +128,7 @@ class VaultCrypto(
|
||||
fun storeEncryptionKey(base64EncryptionKey: String, authMethods: String) {
|
||||
this.encryptionKey = Base64.decode(base64EncryptionKey, Base64.NO_WRAP)
|
||||
|
||||
if (authMethods.contains(BIOMETRICS_AUTH_METHOD) && keystoreProvider.isBiometricAvailable()) {
|
||||
if (authMethods.contains(BIOMETRICS_AUTH_METHOD)) {
|
||||
try {
|
||||
val latch = java.util.concurrent.CountDownLatch(1)
|
||||
var error: Exception? = null
|
||||
|
||||
@@ -478,7 +478,7 @@ class VaultStore(
|
||||
|
||||
auth.setAuthMethods(authMethods)
|
||||
|
||||
if (!wasBiometricEnabled && isBiometricEnabled && crypto.encryptionKey != null && keystoreProvider.isBiometricAvailable()) {
|
||||
if (!wasBiometricEnabled && isBiometricEnabled) {
|
||||
try {
|
||||
crypto.storeEncryptionKey(
|
||||
android.util.Base64.encodeToString(crypto.encryptionKey, android.util.Base64.NO_WRAP),
|
||||
|
||||
@@ -66,14 +66,87 @@ class AndroidKeystoreProvider(
|
||||
|
||||
/**
|
||||
* Whether the biometric is available.
|
||||
* @return Whether the biometric is available
|
||||
* Checks both device biometric support AND validates that the keystore key is valid.
|
||||
* This prevents showing biometric unlock when the key has been invalidated
|
||||
* (e.g., after biometric enrollment changes).
|
||||
* @return Whether the biometric is available and key is valid
|
||||
*/
|
||||
override fun isBiometricAvailable(): Boolean {
|
||||
return _biometricManager.canAuthenticate(
|
||||
// First check if device supports biometrics
|
||||
val deviceSupported = _biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK or
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL,
|
||||
) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
|
||||
if (!deviceSupported) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we have the encrypted key file
|
||||
val keyFile = File(context.filesDir, ENCRYPTED_KEY_FILE)
|
||||
if (!keyFile.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate that the keystore key exists and is not invalidated
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
// Check if key alias exists
|
||||
if (!keyStore.containsAlias(KEYSTORE_ALIAS)) {
|
||||
// Key doesn't exist - clean up orphaned encrypted key file
|
||||
Log.d(TAG, "Keystore key not found, removing orphaned encrypted key file")
|
||||
keyFile.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
val secretKey = keyStore.getKey(KEYSTORE_ALIAS, null) as? SecretKey
|
||||
if (secretKey == null) {
|
||||
Log.d(TAG, "Failed to retrieve keystore key")
|
||||
keyFile.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
// Try to initialize cipher to detect key invalidation
|
||||
val cipher = Cipher.getInstance(
|
||||
"${KeyProperties.KEY_ALGORITHM_AES}/" +
|
||||
"${KeyProperties.BLOCK_MODE_GCM}/" +
|
||||
KeyProperties.ENCRYPTION_PADDING_NONE,
|
||||
)
|
||||
|
||||
// Read IV from encrypted key file for validation
|
||||
val encryptedKeyB64 = keyFile.readText()
|
||||
val combined = Base64.decode(encryptedKeyB64, Base64.NO_WRAP)
|
||||
val byteBuffer = ByteBuffer.wrap(combined)
|
||||
val iv = ByteArray(12)
|
||||
byteBuffer.get(iv)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
|
||||
// Attempt to initialize cipher - this will throw KeyPermanentlyInvalidatedException
|
||||
// if biometric enrollment has changed
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
|
||||
// Key is valid
|
||||
return true
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
// Key has been invalidated due to biometric enrollment change
|
||||
Log.w(TAG, "Keystore key permanently invalidated, cleaning up", e)
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
keyStore.deleteEntry(KEYSTORE_ALIAS)
|
||||
keyFile.delete()
|
||||
} catch (cleanupError: Exception) {
|
||||
Log.e(TAG, "Error during cleanup of invalidated key", cleanupError)
|
||||
}
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
// Any other error means biometric is not available
|
||||
Log.e(TAG, "Error validating biometric availability: ${e.message}", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="240dp"
|
||||
android:height="240dp"
|
||||
android:viewportWidth="700"
|
||||
android:viewportHeight="700">
|
||||
<group
|
||||
android:translateX="100"
|
||||
android:translateY="100">
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="M459.87,294.95c0.016205,5.4005 0.03241,10.801 -0.35022,16.873c-1.111,6.3392 -1.1941,12.173 -2.6351,17.649c-10.922,41.508 -36.731,69.481 -77.351,83.408c-7.2157,2.4739 -14.972,3.3702 -22.479,4.995c-23.629,0.042205 -47.257,0.11453 -70.886,0.12027c-46.762,0.011322 -93.523,-0.01416 -140.95,-0.43411c-8.59,-2.0024 -16.766,-2.8352 -24.398,-5.3326c-21.595,-7.0666 -39.523,-19.656 -53.708,-37.552c-10.227,-12.903 -17.579,-27.17 -21.28,-43.221c-1.475,-6.3967 -2.4711,-12.904 -3.6852,-19.361c-0.051849,-5.747 -0.1037,-11.494 0.26915,-17.886c4.159,-42.973 27.68,-71.638 63.562,-92.153c0,-0.70761 -0.001961,-1.6988 3.12e-4,-2.69c0.022484,-9.8293 -1.3071,-19.894 0.35664,-29.438c3.2391,-18.579 11.08,-35.272 23.763,-49.773c12.098,-13.832 26.457,-23.989 43.609,-30.029c7.813,-2.7512 16.14,-4.0417 24.234,-5.9948c7.392,-0.025734 14.784,-0.05146 22.835,0.32253c4.1959,0.95392 7.7946,1.2538 11.258,2.1053c17.16,4.2192 32.287,12.176 45.469,24.104c2.2558,2.0411 4.372,6.6241 9.621,3.868c16.839,-8.8419 34.718,-11.597 53.603,-8.594c16.791,2.6699 31.602,9.4308 44.236,20.636c11.531,10.227 19.84,22.841 25.393,37.236c6.3436,16.445 10.389,33.163 6.0798,49.389c7.9587,8.9321 15.807,16.704 22.421,25.414c9.162,12.065 15.33,25.746 18.144,40.776c0.97046,5.1848 1.9111,10.375 2.8654,15.563m-71.597,71.012c5.5615,-5.2284 12.002,-9.7986 16.508,-15.817c10.474,-13.992 14.333,-29.916 11.288,-47.446c-2.2496,-12.95 -8.1973,-24.076 -17.243,-33.063c-12.746,-12.663 -28.865,-18.614 -46.786,-18.569c-69.912,0.17712 -139.82,0.56831 -209.74,0.96176c-15.922,0.089599 -29.168,7.4209 -39.685,18.296c-14.45,14.944 -20.408,33.343 -16.655,54.368c2.2763,12.754 8.2167,23.748 17.158,32.66c13.299,13.255 30.097,18.653 48.728,18.651c59.321,-0.005188 118.64,0.042358 177.96,-0.046601c9.5912,-0.014374 19.181,-0.86588 28.773,-0.88855c10.649,-0.025146 19.978,-3.825 29.687,-9.1074z"/>
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="M162.77,293c15.654,4.3883 20.627,22.967 10.304,34.98c-5.3104,6.1795 -14.817,8.3208 -24.278,5.0472c-7.0723,-2.4471 -12.332,-10.362 -12.876,-17.933c-1.0451,-14.542 11.089,-23.176 21.705,-23.046c1.5794,0.019287 3.1517,0.61566 5.1461,0.95184z"/>
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="M227.18,293.64c7.8499,2.3973 11.938,8.2143 13.524,15.077c1.8591,8.0439 -0.44817,15.706 -7.1588,21.121c-6.7633,5.4572 -14.417,6.8794 -22.578,3.1483c-8.2972,-3.7933 -12.836,-10.849 -12.736,-19.438c0.1687,-14.497 14.13,-25.368 28.948,-19.908z"/>
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="M261.57,319.07c-2.495,-14.418 4.6853,-22.603 14.596,-26.108c9.8945,-3.4995 23.181,3.4303 26.267,13.779c4.6504,15.591 -7.1651,29.064 -21.665,28.161c-8.5254,-0.53088 -17.202,-6.5094 -19.198,-15.831z"/>
|
||||
<path
|
||||
android:fillColor="#EEC170"
|
||||
android:pathData="M336.91,333.41c-9.0175,-4.2491 -15.337,-14.349 -13.829,-21.682c3.0825,-14.989 13.341,-20.304 23.018,-19.585c10.653,0.79141 17.93,7.407 19.765,17.547c1.9588,10.824 -4.1171,19.939 -13.494,23.703c-5.272,2.1162 -10.091,1.5086 -15.46,0.017883z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -93,6 +93,8 @@
|
||||
app:hintTextColor="?android:attr/textColorSecondary"
|
||||
app:startIconDrawable="@drawable/ic_lock"
|
||||
app:startIconTint="?android:attr/textColorSecondary"
|
||||
app:endIconMode="password_toggle"
|
||||
app:endIconTint="@color/primary"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@mipmap/ic_launcher_foreground"
|
||||
android:src="@drawable/ic_launcher_foreground"
|
||||
android:contentDescription="@string/aliasvault_icon" />
|
||||
|
||||
<TextView
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Затвори</string>
|
||||
<string name="common_next">Следващ</string>
|
||||
<string name="common_cancel">Отмени</string>
|
||||
<string name="common_back">Back</string>
|
||||
<string name="unknown_error">An unknown error occurred</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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">Отключи</string>
|
||||
<string name="password_unlock_incorrect">Incorrect password. Please try again.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Tanca</string>
|
||||
<string name="common_next">Següent</string>
|
||||
<string name="common_cancel">Cancel·la</string>
|
||||
<string name="common_back">Enrere</string>
|
||||
<string name="unknown_error">An unknown error occurred</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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">Contrasenya</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>
|
||||
|
||||
@@ -7,6 +7,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 -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Luk</string>
|
||||
<string name="common_next">Næste</string>
|
||||
<string name="common_cancel">Annuller</string>
|
||||
<string name="common_back">Tilbage</string>
|
||||
<string name="unknown_error">En ukendt fejl opstod</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Kunne ikke hente, åben app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Bekræft PIN-kode</string>
|
||||
<string name="pin_confirm_description">Indtast din PIN-kode igen for at bekræfte</string>
|
||||
<string name="pin_mismatch">Pinkoden stemmer ikke overens. Prøv venligst igen.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Lås boks op</string>
|
||||
<string name="password_unlock_subtitle">Indtast din hovedadgangskode</string>
|
||||
<string name="password_unlock_password_hint">Adgangskode</string>
|
||||
<string name="password_unlock_button">Lås op</string>
|
||||
<string name="password_unlock_incorrect">Forkert adgangskode. Prøv venligst igen.</string>
|
||||
<string name="password_unlock_error">Kunne ikke bekræfte adgangskode</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Schließen</string>
|
||||
<string name="common_next">Weiter</string>
|
||||
<string name="common_cancel">Abbrechen</string>
|
||||
<string name="common_back">Zurück</string>
|
||||
<string name="unknown_error">Ein unbekannter Fehler ist aufgetreten</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Abruf der Daten fehlgeschlagen. Öffne die App</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">PIN bestätigen</string>
|
||||
<string name="pin_confirm_description">Zur Bestätigung PIN erneut eingeben</string>
|
||||
<string name="pin_mismatch">PINs stimmen nicht überein. Bitte versuche es erneut.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Tresor entsperren</string>
|
||||
<string name="password_unlock_subtitle">Gib Dein Master-Passwort ein</string>
|
||||
<string name="password_unlock_password_hint">Passwort</string>
|
||||
<string name="password_unlock_button">Entsperren</string>
|
||||
<string name="password_unlock_incorrect">Falsches Passwort. Bitte versuche es erneut.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Cerrar</string>
|
||||
<string name="common_next">Siguiente</string>
|
||||
<string name="common_cancel">Cancelar</string>
|
||||
<string name="common_back">Atrás</string>
|
||||
<string name="unknown_error">Se produjo un error desconocido</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Fallo al recuperar, abrir aplicación</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Confirmar PIN</string>
|
||||
<string name="pin_confirm_description">Vuelva a introducir su PIN para confirmar</string>
|
||||
<string name="pin_mismatch">Los PIN no coinciden. Por favor, inténtalo de nuevo.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Desbloquear bóveda</string>
|
||||
<string name="password_unlock_subtitle">Ingrese su contraseña maestra</string>
|
||||
<string name="password_unlock_password_hint">Contraseña</string>
|
||||
<string name="password_unlock_button">Desbloquear</string>
|
||||
<string name="password_unlock_incorrect">Contraseña incorrecta. Por favor, inténtelo de nuevo.</string>
|
||||
<string name="password_unlock_error">Error al verificar la contraseña</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,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 -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Sulje</string>
|
||||
<string name="common_next">Seuraava</string>
|
||||
<string name="common_cancel">Peruuta</string>
|
||||
<string name="common_back">Takaisin</string>
|
||||
<string name="unknown_error">Tapahtui tuntematon virhe</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Nouto epäonnistui, avaa sovellus</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Vahvista PIN</string>
|
||||
<string name="pin_confirm_description">Syötä PIN-koodi uudelleen vahvistaaksesi</string>
|
||||
<string name="pin_mismatch">PIN-koodit eivät täsmää. Yritä uudelleen.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Avaa holvin lukitus</string>
|
||||
<string name="password_unlock_subtitle">Syötä pääsalasanasi</string>
|
||||
<string name="password_unlock_password_hint">Salasana:</string>
|
||||
<string name="password_unlock_button">Avaa lukitus</string>
|
||||
<string name="password_unlock_incorrect">Virheellinen salasana. Yritä uudelleen. </string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Fermer</string>
|
||||
<string name="common_next">Suivant</string>
|
||||
<string name="common_cancel">Annuler</string>
|
||||
<string name="common_back">Retour</string>
|
||||
<string name="unknown_error">Une erreur inconnue s\'est produite</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Échec de la récupération, ouvrez l\'application</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Confirmer le code PIN</string>
|
||||
<string name="pin_confirm_description">Entrez à nouveau votre code PIN pour confirmer</string>
|
||||
<string name="pin_mismatch">Les codes PIN ne correspondent pas. Veuillez réessayer.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Déverrouiller le coffre</string>
|
||||
<string name="password_unlock_subtitle">Entrez votre mot de passe maître</string>
|
||||
<string name="password_unlock_password_hint">Mot de passe</string>
|
||||
<string name="password_unlock_button">Déverrouiller</string>
|
||||
<string name="password_unlock_incorrect">Mot de passe incorrect. Veuillez réessayer.</string>
|
||||
<string name="password_unlock_error">Échec de la vérification du mot de passe</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">סגירה</string>
|
||||
<string name="common_next">הבא</string>
|
||||
<string name="common_cancel">ביטול</string>
|
||||
<string name="common_back">חזרה</string>
|
||||
<string name="unknown_error">אירעה שגיאה לא ידועה</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">המשיכה נכשלה, נא לפתוח את היישום</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">אישור קוד אישי</string>
|
||||
<string name="pin_confirm_description">נא למלא את הקוד האישי שלך מחדש לאישור</string>
|
||||
<string name="pin_mismatch">הקודים האישיים לא תואמים. נא לנסות שוב.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">שחרור נעילת כספת</string>
|
||||
<string name="password_unlock_subtitle">נא למלא את סיסמת העל שלך</string>
|
||||
<string name="password_unlock_password_hint">סיסמה</string>
|
||||
<string name="password_unlock_button">שחרור נעילה</string>
|
||||
<string name="password_unlock_incorrect">סיסמה שגויה. נא לנסות שוב.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Chiudi</string>
|
||||
<string name="common_next">Avanti</string>
|
||||
<string name="common_cancel">Annulla</string>
|
||||
<string name="common_back">Indietro</string>
|
||||
<string name="unknown_error">Si è verificato un errore sconosciuto</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Impossibile recuperare, aprire l\'app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Conferma PIN</string>
|
||||
<string name="pin_confirm_description">Reinserisci il tuo PIN per confermare.</string>
|
||||
<string name="pin_mismatch">I PIN non corrispondono. Riprova.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Sblocca Cassaforte</string>
|
||||
<string name="password_unlock_subtitle">Inserisci la password principale</string>
|
||||
<string name="password_unlock_password_hint">Password</string>
|
||||
<string name="password_unlock_button">Sblocca</string>
|
||||
<string name="password_unlock_incorrect">Password errata. Riprovare.</string>
|
||||
<string name="password_unlock_error">Verifica della password non riuscita</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,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 -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<resources>
|
||||
<!-- Splashscreen and autofill background (dark mode) -->
|
||||
<color name="splashscreen_background">#202020</color>
|
||||
|
||||
<!-- AliasVault Dark Mode Colors -->
|
||||
<color name="av_text">#ECEDEE</color>
|
||||
<color name="av_text_muted">#9BA1A6</color>
|
||||
<color name="av_background">#000000</color>
|
||||
@@ -14,7 +11,5 @@
|
||||
<color name="av_tertiary">#eabf69</color>
|
||||
<color name="av_icon">#9BA1A6</color>
|
||||
<color name="av_error">#ef4444</color>
|
||||
|
||||
<!-- Loading overlay background (dark mode: black with 80% opacity) -->
|
||||
<color name="av_loading_overlay">#CC000000</color>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -23,4 +23,13 @@
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<!-- Dark mode splash screen theme -->
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="windowSplashScreenAnimationDuration">0</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">@android:color/transparent</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Sluiten</string>
|
||||
<string name="common_next">Volgende</string>
|
||||
<string name="common_cancel">Annuleren</string>
|
||||
<string name="common_back">Terug</string>
|
||||
<string name="unknown_error">Er is een onbekende fout opgetreden</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Ophalen mislukt, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Bevestig pincode</string>
|
||||
<string name="pin_confirm_description">Voer je pincode opnieuw in om te bevestigen</string>
|
||||
<string name="pin_mismatch">Pincodes komen niet overeen. Probeer het opnieuw.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Ontgrendel je vault</string>
|
||||
<string name="password_unlock_subtitle">Voer je hoofdwachtwoord in</string>
|
||||
<string name="password_unlock_password_hint">Wachtwoord</string>
|
||||
<string name="password_unlock_button">Ontgrendelen</string>
|
||||
<string name="password_unlock_incorrect">Onjuist wachtwoord. Probeer het opnieuw.</string>
|
||||
<string name="password_unlock_error">Verifiëren wachtwoord mislukt</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Zamknij</string>
|
||||
<string name="common_next">Dalej</string>
|
||||
<string name="common_cancel">Anuluj</string>
|
||||
<string name="common_back">Wstecz</string>
|
||||
<string name="unknown_error">Wystąpił nieznany błąd</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Nie udało się pobrać, otwórz aplikację</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Potwierdź kod PIN</string>
|
||||
<string name="pin_confirm_description">Wprowadź ponownie swój kod PIN, aby potwierdzić</string>
|
||||
<string name="pin_mismatch">Kody PIN nie pasują. Spróbuj jeszcze raz.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Odblokuj sejf</string>
|
||||
<string name="password_unlock_subtitle">Wprowadź swoje hasło główne</string>
|
||||
<string name="password_unlock_password_hint">Hasło</string>
|
||||
<string name="password_unlock_button">Odblokuj</string>
|
||||
<string name="password_unlock_incorrect">Nieprawidłowe hasło. Spróbuj ponownie.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Fechar</string>
|
||||
<string name="common_next">Próximo</string>
|
||||
<string name="common_cancel">Cancelar</string>
|
||||
<string name="common_back">Voltar</string>
|
||||
<string name="unknown_error">Ocorreu um erro desconhecido</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Falha ao recuperar, abra o aplicativo</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Confirmar PIN</string>
|
||||
<string name="pin_confirm_description">Digite seu PIN novamente para confirmar</string>
|
||||
<string name="pin_mismatch">PINs não correspondem. Por favor, tente novamente.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Desbloquear Cofre</string>
|
||||
<string name="password_unlock_subtitle">Digite sua senha mestre</string>
|
||||
<string name="password_unlock_password_hint">Senha</string>
|
||||
<string name="password_unlock_button">Desbloquear</string>
|
||||
<string name="password_unlock_incorrect">Senha incorreta. Por favor tente novamente.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Închide</string>
|
||||
<string name="common_next">Înainte</string>
|
||||
<string name="common_cancel">Anulează</string>
|
||||
<string name="common_back">Înapoi</string>
|
||||
<string name="unknown_error">A apărut o eroare necunoscută</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Preluare eșuată, deschide aplicația</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Confirmă PIN</string>
|
||||
<string name="pin_confirm_description">Introdu PIN-ul din nou pentru confirmare</string>
|
||||
<string name="pin_mismatch">Codurile PIN nu se potrivesc. Încearcă din nou.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Deblochează seiful</string>
|
||||
<string name="password_unlock_subtitle">Introdu parola principală</string>
|
||||
<string name="password_unlock_password_hint">Parolă</string>
|
||||
<string name="password_unlock_button">Deblochează</string>
|
||||
<string name="password_unlock_incorrect">Parolă incorectă. Încearcă din nou.</string>
|
||||
<string name="password_unlock_error">Verificarea parolei a eșuat</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Закрыть</string>
|
||||
<string name="common_next">Далее</string>
|
||||
<string name="common_cancel">Отмена</string>
|
||||
<string name="common_back">Назад</string>
|
||||
<string name="unknown_error">Произошла неизвестная ошибка</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Не удалось извлечь, открыть приложение</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Подтвердите ПИН-код</string>
|
||||
<string name="pin_confirm_description">Введите PIN-код еще раз для подтверждения</string>
|
||||
<string name="pin_mismatch">PIN-коды не совпадают. Повторите попытку.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Разблокировать хранилище</string>
|
||||
<string name="password_unlock_subtitle">Введите свой мастер-пароль</string>
|
||||
<string name="password_unlock_password_hint">Пароль</string>
|
||||
<string name="password_unlock_button">Разблокировать</string>
|
||||
<string name="password_unlock_incorrect">Неверный пароль. Повторите попытку.</string>
|
||||
<string name="password_unlock_error">Не удалось проверить пароль</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Stäng</string>
|
||||
<string name="common_next">Nästa</string>
|
||||
<string name="common_cancel">Avbryt</string>
|
||||
<string name="common_back">Tillbaka</string>
|
||||
<string name="unknown_error">Ett okänt fel har inträffat</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Det gick inte att hämta, öppna app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Bekräfta PIN-kod</string>
|
||||
<string name="pin_confirm_description">Ange din PIN-kod igen för att bekräfta</string>
|
||||
<string name="pin_mismatch">PIN-koderna matchar inte. Var god försök igen.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Lås upp valv</string>
|
||||
<string name="password_unlock_subtitle">Ange ditt huvudlösenord</string>
|
||||
<string name="password_unlock_password_hint">Lösenord</string>
|
||||
<string name="password_unlock_button">Lås upp</string>
|
||||
<string name="password_unlock_incorrect">Felaktigt lösenord. Försök igen.</string>
|
||||
<string name="password_unlock_error">Kunde inte verifiera lösenord</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Kapat</string>
|
||||
<string name="common_next">İleri</string>
|
||||
<string name="common_cancel">İptal</string>
|
||||
<string name="common_back">Geri</string>
|
||||
<string name="unknown_error">An unknown error occurred</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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">Kasa Kilidini Aç</string>
|
||||
<string name="password_unlock_subtitle">Enter your master password</string>
|
||||
<string name="password_unlock_password_hint">Parola</string>
|
||||
<string name="password_unlock_button">Kilidi aç</string>
|
||||
<string name="password_unlock_incorrect">Parola yanlış. Lütfen yeniden deneyin.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">Закрити</string>
|
||||
<string name="common_next">Далі</string>
|
||||
<string name="common_cancel">Скасувати</string>
|
||||
<string name="common_back">Назад</string>
|
||||
<string name="unknown_error">Сталася невідома помилка</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Не вдалося отримати, відкрийте додаток</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">Підтвердити ПІН-код</string>
|
||||
<string name="pin_confirm_description">Введіть PIN-код ще раз для підтвердження</string>
|
||||
<string name="pin_mismatch">PIN-коди не збігаються. Спробуйте ще раз.</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">Розблокувати Vault</string>
|
||||
<string name="password_unlock_subtitle">Введіть ваш головний пароль</string>
|
||||
<string name="password_unlock_password_hint">Пароль</string>
|
||||
<string name="password_unlock_button">Розблокувати</string>
|
||||
<string name="password_unlock_incorrect">Невірний пароль. Будь ласка, спробуйте ще раз.</string>
|
||||
<string name="password_unlock_error">Failed to verify password</string>
|
||||
</resources>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">بند کریں</string>
|
||||
<string name="common_next">آگے جائیں</string>
|
||||
<string name="common_cancel">منسوخ کریں</string>
|
||||
<string name="common_back">واپس جائیں</string>
|
||||
<string name="unknown_error">An unknown error occurred</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<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">پاس ورڈ</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>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<string name="common_close">关闭</string>
|
||||
<string name="common_next">下一步</string>
|
||||
<string name="common_cancel">取消</string>
|
||||
<string name="common_back">返回</string>
|
||||
<string name="unknown_error">发生未知错误</string>
|
||||
<!-- AutofillService strings -->
|
||||
<string name="autofill_failed_to_retrieve">检索失败,请打开应用</string>
|
||||
@@ -81,4 +82,11 @@
|
||||
<string name="pin_confirm_title">确认 PIN</string>
|
||||
<string name="pin_confirm_description">重新输入您的 PIN 以确认</string>
|
||||
<string name="pin_mismatch">PIN 不匹配,请重试。</string>
|
||||
<!-- Password unlock -->
|
||||
<string name="password_unlock_title">解锁密码库</string>
|
||||
<string name="password_unlock_subtitle">输入您的主密码</string>
|
||||
<string name="password_unlock_password_hint">密码</string>
|
||||
<string name="password_unlock_button">解锁</string>
|
||||
<string name="password_unlock_incorrect">密码错误,请重试。</string>
|
||||
<string name="password_unlock_error">密码验证失败</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
|
||||
<!-- Legacy color -->
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
|
||||
<!-- AliasVault Light Mode Colors -->
|
||||
<color name="av_light_text">#11181C</color>
|
||||
<color name="av_light_text_muted">#4b5563</color>
|
||||
<color name="av_light_background">#f3f4f6</color>
|
||||
@@ -16,8 +12,6 @@
|
||||
<color name="av_light_tertiary">#eabf69</color>
|
||||
<color name="av_light_icon">#687076</color>
|
||||
<color name="av_light_error">#D32F2F</color>
|
||||
|
||||
<!-- AliasVault Dark Mode Colors (in values/colors.xml, will be overridden in values-night/colors.xml) -->
|
||||
<color name="av_text">#11181C</color>
|
||||
<color name="av_text_muted">#4b5563</color>
|
||||
<color name="av_background">#f3f4f6</color>
|
||||
@@ -29,10 +23,8 @@
|
||||
<color name="av_tertiary">#eabf69</color>
|
||||
<color name="av_icon">#687076</color>
|
||||
<color name="av_error">#D32F2F</color>
|
||||
|
||||
<!-- Loading overlay background (light mode: white with 80% opacity) -->
|
||||
<color name="av_loading_overlay">#CCFFFFFF</color>
|
||||
|
||||
<!-- Alias for primary color (used in PIN unlock) -->
|
||||
<color name="primary">@color/av_primary</color>
|
||||
</resources>
|
||||
<color name="iconBackground">#000000</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
</resources>
|
||||
@@ -5,27 +5,19 @@
|
||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||
<string name="autofill_service_description" translatable="true">AliasVault AutoFill</string>
|
||||
<string name="aliasvault_icon">AliasVault icon</string>
|
||||
|
||||
<!-- Common strings -->
|
||||
<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 -->
|
||||
<string name="autofill_failed_to_retrieve">Failed to retrieve, open app</string>
|
||||
<string name="autofill_no_match_found">No match found, create new?</string>
|
||||
<string name="autofill_open_app">Open app</string>
|
||||
<string name="autofill_vault_locked">Vault locked</string>
|
||||
|
||||
<!-- Biometric prompts -->
|
||||
<string name="biometric_store_key_title">Store Encryption Key</string>
|
||||
<string name="biometric_store_key_subtitle">Authenticate to securely store your encryption key in the Android Keystore. This enables secure access to your vault.</string>
|
||||
<string name="biometric_unlock_vault_title">Unlock Vault</string>
|
||||
<string name="biometric_unlock_vault_subtitle">Authenticate to access your vault</string>
|
||||
|
||||
<!-- Passkey registration -->
|
||||
<string name="passkey_registration_title">Create Passkey</string>
|
||||
<string name="create_passkey_title">Create New Passkey</string>
|
||||
<string name="create_passkey_subtitle">Register a new passkey for this website. It will be securely stored in your vault and automatically synced across your devices with AliasVault.</string>
|
||||
@@ -56,8 +48,6 @@
|
||||
<string name="passkey_retrieving">Retrieving passkey…</string>
|
||||
<string name="passkey_verifying">Verifying…</string>
|
||||
<string name="passkey_authenticating">Authenticating…</string>
|
||||
|
||||
<!-- Vault sync error messages -->
|
||||
<string name="connection_error_title">Connection Error</string>
|
||||
<string name="connection_error_message">No connection to the server can be made. Please check your internet connection and try creating the passkey again.</string>
|
||||
<string name="session_expired_title">Session Expired</string>
|
||||
@@ -72,32 +62,26 @@
|
||||
<string name="network_error_message">A network error occurred. Please check your connection and try again.</string>
|
||||
<string name="server_version_not_supported_title">Server Update Required</string>
|
||||
<string name="server_version_not_supported_message">The server version is outdated. Please contact your administrator to update the server.</string>
|
||||
|
||||
<!-- Passkey authentication and unlock error messages -->
|
||||
<string name="error_unlock_method_required">Please enable biometric or PIN authentication in the main AliasVault app in order to continue</string>
|
||||
<string name="error_unlock_vault_first">Please unlock vault in AliasVault app first</string>
|
||||
<string name="error_vault_decrypt_failed">Failed to decrypt vault</string>
|
||||
<string name="error_biometric_cancelled">Biometric authentication cancelled</string>
|
||||
<string name="error_encryption_key_failed">Failed to retrieve encryption key</string>
|
||||
|
||||
<!-- PIN unlock -->
|
||||
<string name="pin_unlock_vault">Unlock Vault</string>
|
||||
<string name="pin_enter_to_unlock">Enter your PIN to unlock your vault</string>
|
||||
<string name="pin_locked_max_attempts">PIN locked after too many failed attempts</string>
|
||||
<string name="pin_incorrect_attempts_remaining">Incorrect PIN. %d attempts remaining</string>
|
||||
|
||||
<!-- PIN setup -->
|
||||
<string name="pin_setup_title">Setup PIN</string>
|
||||
<string name="pin_setup_description">Choose a PIN to unlock your vault</string>
|
||||
<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>
|
||||
<string name="password_unlock_attempts_warning">Incorrect password. You will be logged out if you enter the wrong password %d more times.</string>
|
||||
<string name="password_unlock_max_attempts_reached">Too many failed unlock attempts. You have been logged out for security reasons.</string>
|
||||
</resources>
|
||||
@@ -1,25 +1,15 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="AppTheme" parent="Theme.EdgeToEdge.Light">
|
||||
<style name="AppTheme" parent="Theme.EdgeToEdge">
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
<item name="android:editTextStyle">@style/ResetEditText</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">@color/av_background</item>
|
||||
<item name="android:navigationBarColor">#fff</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<!-- Light mode: use dark icons/text on light background -->
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
<item name="enforceNavigationBarContrast">false</item>
|
||||
</style>
|
||||
<style name="ResetEditText" parent="@android:style/Widget.EditText">
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:textColorHint">#c8c8c8</item>
|
||||
<item name="android:textColor">@android:color/black</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
<style name="PasskeyRegistrationTheme" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/av_primary</item>
|
||||
<item name="colorOnPrimary">#FFFFFF</item>
|
||||
@@ -47,4 +37,11 @@
|
||||
<item name="android:insetBottom">2dp</item>
|
||||
<item name="rippleColor">@color/av_primary</item>
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/splashscreen_background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/splashscreen_logo</item>
|
||||
<item name="windowSplashScreenAnimationDuration">0</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">@android:color/transparent</item>
|
||||
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -60,3 +60,6 @@ expo.useLegacyPackaging=false
|
||||
|
||||
# Workaround for Expo modules compatibility with Android Gradle Plugin 8.x
|
||||
android.nonTransitiveRClass=false
|
||||
|
||||
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
|
||||
expo.edgeToEdgeEnabled=true
|
||||
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "AliasVault",
|
||||
"slug": "AliasVault",
|
||||
"version": "0.27.0-alpha",
|
||||
"version": "0.28.0-alpha",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "aliasvault",
|
||||
@@ -28,7 +28,17 @@
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"package": "net.aliasvault.app"
|
||||
"package": "net.aliasvault.app",
|
||||
"splash": {
|
||||
"image": "./assets/images/adaptive-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff",
|
||||
"dark": {
|
||||
"image": "./assets/images/adaptive-icon.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#202020"
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
@@ -37,15 +47,6 @@
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/images/adaptive-icon.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
],
|
||||
[
|
||||
"react-native-edge-to-edge",
|
||||
{
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useNavigation } from 'expo-router';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, ScrollView, RefreshControl, Animated , Platform } from 'react-native';
|
||||
import { StyleSheet, Platform, View, ScrollView, RefreshControl, Animated } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { MailboxBulkRequest, MailboxBulkResponse, MailboxEmail } from '@/utils/dist/core/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -145,11 +145,7 @@ export default function EmailsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = useCallback(async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
} else if (Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsLoading(true);
|
||||
setIsRefreshing(true);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ItemTypes, getSystemFieldsForItemType, getOptionalFieldsForItemType, is
|
||||
import type { FaviconExtractModel } from '@/utils/dist/core/models/webapi';
|
||||
import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/core/password-generator';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { extractServiceNameFromUrl } from '@/utils/UrlUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -671,9 +672,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
Fields: []
|
||||
});
|
||||
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
}, [item, isEditMode]);
|
||||
|
||||
/**
|
||||
@@ -915,9 +914,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
setIsSaveDisabled(false);
|
||||
|
||||
// Haptic feedback for successful save
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
// Navigate immediately - sync continues in background
|
||||
if (itemUrl && !isEditMode) {
|
||||
@@ -982,9 +979,7 @@ export default function AddEditItemScreen(): React.ReactNode {
|
||||
emitter.emit('credentialChanged', id);
|
||||
|
||||
// Haptic feedback for delete action (warning type for destructive action)
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Warning);
|
||||
|
||||
setTimeout(() => {
|
||||
Toast.show({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
|
||||
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -227,9 +227,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
* Handle pull-to-refresh.
|
||||
*/
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setRefreshing(true);
|
||||
setIsLoadingItems(true);
|
||||
@@ -404,7 +402,7 @@ export default function FolderViewScreen(): React.ReactNode {
|
||||
const handleAddItem = useCallback(() => {
|
||||
navigate(() => {
|
||||
router.push(`/(tabs)/items/add-edit?folderId=${folderId}` as '/(tabs)/items/add-edit');
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
HapticsUtility.impact();
|
||||
});
|
||||
}, [folderId, router, navigate]);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,6 +12,7 @@ import type { CredentialSortOrder } from '@/utils/db/repositories/SettingsReposi
|
||||
import type { Item, ItemType } from '@/utils/dist/core/models/vault';
|
||||
import { getFieldValue, FieldKey, ItemTypes } from '@/utils/dist/core/models/vault';
|
||||
import emitter from '@/utils/EventEmitter';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { VaultAuthenticationError } from '@/utils/types/errors/VaultAuthenticationError';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -367,9 +367,7 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
* Handle pull-to-refresh.
|
||||
*/
|
||||
const onRefresh = useCallback(async () => {
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setRefreshing(true);
|
||||
setIsLoadingItems(true);
|
||||
@@ -549,7 +547,7 @@ export default function ItemsScreen(): React.ReactNode {
|
||||
} else {
|
||||
router.push('/(tabs)/items/add-edit');
|
||||
}
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
HapticsUtility.impact();
|
||||
});
|
||||
}, [router, searchQuery, navigate]);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { AppUnlockUtility } from '@/utils/AppUnlockUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useLogout } from '@/hooks/useLogout';
|
||||
@@ -31,7 +32,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
const { t } = useTranslation();
|
||||
const { showAlert, showConfirm } = useDialog();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { getAuthMethodDisplayKey, shouldShowAutofillReminder } = useApp();
|
||||
const { shouldShowAutofillReminder } = useApp();
|
||||
const { getAutoLockTimeout } = useApp();
|
||||
const { logoutUserInitiated } = useLogout();
|
||||
const { loadApiUrl, getDisplayUrl } = useApiUrl();
|
||||
@@ -97,7 +98,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
* Load the auth method display.
|
||||
*/
|
||||
const loadAuthMethodDisplay = async () : Promise<void> => {
|
||||
const authMethodKey = await getAuthMethodDisplayKey();
|
||||
const authMethodKey = await AppUnlockUtility.getAuthMethodDisplayKey();
|
||||
setAuthMethodDisplay(t(authMethodKey));
|
||||
};
|
||||
|
||||
@@ -110,7 +111,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, t])
|
||||
}, [getAutoLockTimeout, setIsFirstLoad, loadApiUrl, t])
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -282,8 +283,11 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
},
|
||||
settingItemValue: {
|
||||
color: colors.textMuted,
|
||||
flexShrink: 1,
|
||||
fontSize: 16,
|
||||
marginRight: 8,
|
||||
maxWidth: '50%',
|
||||
textAlign: 'right',
|
||||
},
|
||||
skeletonLoader: {
|
||||
marginRight: 8,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, TouchableOpacity, RefreshControl, Platform } from 'react-native';
|
||||
import { StyleSheet, View, TouchableOpacity, RefreshControl } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { RefreshToken } from '@/utils/dist/core/models/webapi';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -27,6 +27,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
const [refreshTokens, setRefreshTokens] = useState<RefreshToken[]>([]);
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 200);
|
||||
const [isRefreshing, setIsRefreshing] = useMinDurationLoading(false, 200);
|
||||
const [expandedSessions, setExpandedSessions] = useState<Set<string>>(new Set());
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
detailText: {
|
||||
@@ -135,9 +136,7 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsRefreshing(true);
|
||||
await loadSessions();
|
||||
@@ -158,6 +157,25 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
return dateObject.toISOString().slice(0, 16).replace('T', ' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the expansion state of a session.
|
||||
* @param sessionId - The session ID to toggle
|
||||
*/
|
||||
const toggleSessionExpansion = (sessionId: string) : void => {
|
||||
setExpandedSessions((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sessionId)) {
|
||||
newSet.delete(sessionId);
|
||||
} else {
|
||||
newSet.add(sessionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Trigger haptic feedback when toggling
|
||||
HapticsUtility.impact();
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedContainer>
|
||||
<ThemedScrollView
|
||||
@@ -184,7 +202,18 @@ export default function ActiveSessionsScreen() : React.ReactNode {
|
||||
refreshTokens.map((item) => (
|
||||
<View key={item.id} style={styles.sessionItem}>
|
||||
<View style={styles.sessionHeader}>
|
||||
<ThemedText style={styles.deviceName} numberOfLines={2}>{item.deviceIdentifier}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={() => toggleSessionExpansion(item.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ThemedText
|
||||
style={styles.deviceName}
|
||||
numberOfLines={expandedSessions.has(item.id) ? undefined : 2}
|
||||
>
|
||||
{item.deviceIdentifier}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => handleRevokeSession(item.id)}>
|
||||
<ThemedText style={styles.revokeButton}>{t('settings.securitySettings.activeSessions.revoke')}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, RefreshControl, Platform } from 'react-native';
|
||||
import { StyleSheet, View, RefreshControl } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import type { AuthLogModel } from '@/utils/dist/core/models/webapi';
|
||||
import { AuthEventType } from '@/utils/dist/core/models/webapi';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
|
||||
@@ -109,9 +109,7 @@ export default function AuthLogsScreen() : React.ReactNode {
|
||||
*/
|
||||
const onRefresh = async () : Promise<void> => {
|
||||
// Trigger haptic feedback when pull-to-refresh is activated
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
setIsRefreshing(true);
|
||||
await loadLogs();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as LocalAuthentication from 'expo-local-authentication';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StyleSheet, View, Platform, Linking, Switch, TouchableOpacity } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { AppUnlockUtility } from '@/utils/AppUnlockUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { ThemedContainer } from '@/components/themed/ThemedContainer';
|
||||
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
|
||||
import { ThemedText } from '@/components/themed/ThemedText';
|
||||
import { AuthMethod, useAuth } from '@/context/AuthContext';
|
||||
import { useDialog } from '@/context/DialogContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
@@ -20,12 +20,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { showAlert, showDialog } = useDialog();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const { setAuthMethods, getEnabledAuthMethods, getBiometricDisplayName } = useAuth();
|
||||
const [hasBiometrics, setHasBiometrics] = useState(false);
|
||||
const [isBiometricsEnabled, setIsBiometricsEnabled] = useState(false);
|
||||
const [biometricDisplayName, setBiometricDisplayName] = useState('');
|
||||
const [_, setEnabledAuthMethods] = useState<AuthMethod[]>([]);
|
||||
|
||||
// PIN state
|
||||
const [pinEnabled, setPinEnabled] = useState(false);
|
||||
@@ -36,64 +33,44 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
*/
|
||||
const initializeAuth = async () : Promise<void> => {
|
||||
try {
|
||||
// Check for hardware support
|
||||
const compatible = await LocalAuthentication.hasHardwareAsync();
|
||||
// Check if device has biometric hardware and enrollment
|
||||
const deviceAvailable = await AppUnlockUtility.isBiometricsAvailableOnDevice();
|
||||
setHasBiometrics(deviceAvailable);
|
||||
|
||||
// Check if any biometrics are enrolled
|
||||
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
||||
|
||||
// Set biometric availability based on all checks
|
||||
const isBiometricAvailable = compatible && enrolled;
|
||||
setHasBiometrics(isBiometricAvailable);
|
||||
|
||||
// Get appropriate display name from auth context
|
||||
const displayName = await getBiometricDisplayName();
|
||||
// Get appropriate display name
|
||||
const displayName = await AppUnlockUtility.getBiometricDisplayName();
|
||||
setBiometricDisplayName(displayName);
|
||||
|
||||
const methods = await getEnabledAuthMethods();
|
||||
setEnabledAuthMethods(methods);
|
||||
const methods = await AppUnlockUtility.getEnabledAuthMethods();
|
||||
|
||||
if (methods.includes('faceid') && enrolled) {
|
||||
setIsBiometricsEnabled(true);
|
||||
// Check if biometric unlock is actually functional (validates stored key)
|
||||
if (methods.includes('faceid') && deviceAvailable) {
|
||||
const unlockAvailable = await AppUnlockUtility.isBiometricUnlockAvailable();
|
||||
|
||||
if (!unlockAvailable) {
|
||||
/*
|
||||
* Key is invalid (e.g., biometric enrollment changed)
|
||||
* Remove biometrics from auth methods so user must re-enable it
|
||||
*/
|
||||
console.info('Biometric key invalid, removing from auth methods');
|
||||
await AppUnlockUtility.disableAuthMethod('faceid');
|
||||
setIsBiometricsEnabled(false);
|
||||
} else {
|
||||
setIsBiometricsEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Load PIN settings (locked state removed - automatically handled by native code)
|
||||
// Load PIN settings
|
||||
const enabled = await NativeVaultManager.isPinEnabled();
|
||||
setPinEnabled(enabled);
|
||||
|
||||
setInitialized(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize auth:', error);
|
||||
setHasBiometrics(false);
|
||||
setInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
}, [getEnabledAuthMethods, getBiometricDisplayName, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the auth methods.
|
||||
*/
|
||||
const updateAuthMethods = async () : Promise<void> => {
|
||||
const currentAuthMethods = await getEnabledAuthMethods();
|
||||
const newAuthMethods = isBiometricsEnabled ? ['faceid', 'password'] : ['password'];
|
||||
|
||||
if (currentAuthMethods.length === newAuthMethods.length &&
|
||||
currentAuthMethods.every(method => newAuthMethods.includes(method))) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthMethods(newAuthMethods as AuthMethod[]);
|
||||
};
|
||||
|
||||
updateAuthMethods();
|
||||
}, [isBiometricsEnabled, setAuthMethods, getEnabledAuthMethods, initialized]);
|
||||
}, [t]);
|
||||
|
||||
const handleBiometricsToggle = useCallback(async (value: boolean) : Promise<void> => {
|
||||
if (value && !hasBiometrics) {
|
||||
@@ -107,9 +84,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
/**
|
||||
* Handle the open settings press.
|
||||
*/
|
||||
onPress: () : void => {
|
||||
onPress: async () : Promise<void> => {
|
||||
await AppUnlockUtility.enableAuthMethod('faceid');
|
||||
setIsBiometricsEnabled(true);
|
||||
setAuthMethods(['faceid', 'password']);
|
||||
if (Platform.OS === 'ios') {
|
||||
Linking.openURL('app-settings:');
|
||||
} else {
|
||||
@@ -123,9 +100,9 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
/**
|
||||
* Handle the cancel press.
|
||||
*/
|
||||
onPress: () : void => {
|
||||
onPress: async () : Promise<void> => {
|
||||
await AppUnlockUtility.disableAuthMethod('faceid');
|
||||
setIsBiometricsEnabled(false);
|
||||
setAuthMethods(['password']);
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -133,8 +110,8 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if keystore is available when enabling biometrics (iOS requires device passcode)
|
||||
if (value && Platform.OS === 'ios') {
|
||||
// Check if keystore is available when enabling biometrics (requires device passcode)
|
||||
if (value) {
|
||||
const keystoreAvailable = await NativeVaultManager.isKeystoreAvailable();
|
||||
if (!keystoreAvailable) {
|
||||
showAlert(
|
||||
@@ -146,13 +123,16 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
/*
|
||||
* Biometrics and PIN can now both be enabled simultaneously.
|
||||
* Biometrics takes priority during unlock, PIN serves as fallback.
|
||||
* Save new biometrics state.
|
||||
*/
|
||||
if (value) {
|
||||
await AppUnlockUtility.enableAuthMethod('faceid');
|
||||
} else {
|
||||
await AppUnlockUtility.disableAuthMethod('faceid');
|
||||
}
|
||||
setIsBiometricsEnabled(value);
|
||||
setAuthMethods(value ? ['faceid', 'password'] : ['password']);
|
||||
|
||||
// Show toast notification only on biometrics enabled
|
||||
// Show toast notification
|
||||
if (value) {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
@@ -161,7 +141,7 @@ export default function VaultUnlockSettingsScreen() : React.ReactNode {
|
||||
visibilityTime: 1200,
|
||||
});
|
||||
}
|
||||
}, [hasBiometrics, setAuthMethods, biometricDisplayName, showDialog, showAlert, t]);
|
||||
}, [hasBiometrics, biometricDisplayName, showDialog, showAlert, t]);
|
||||
|
||||
/**
|
||||
* Handle enable PIN - launches native PIN setup UI.
|
||||
|
||||
@@ -8,6 +8,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Animated, ScrollView, KeyboardAvoidingView, Platform, Dimensions } from 'react-native';
|
||||
|
||||
import { useApiUrl } from '@/utils/ApiUrlUtility';
|
||||
import { AppUnlockUtility } from '@/utils/AppUnlockUtility';
|
||||
import ConversionUtility from '@/utils/ConversionUtility';
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/core/models/metadata';
|
||||
import type { LoginResponse } from '@/utils/dist/core/models/webapi';
|
||||
@@ -166,9 +167,9 @@ export default function LoginScreen() : React.ReactNode {
|
||||
passwordHashBase64: string,
|
||||
initiateLoginResponse: LoginResponse
|
||||
) : Promise<void> => {
|
||||
// Get biometric display name from auth context
|
||||
const biometricDisplayName = await authContext.getBiometricDisplayName();
|
||||
const isBiometricsEnabledOnDevice = await authContext.isBiometricsEnabledOnDevice();
|
||||
// Get biometric display name
|
||||
const biometricDisplayName = await AppUnlockUtility.getBiometricDisplayName();
|
||||
const isBiometricsEnabledOnDevice = await AppUnlockUtility.isBiometricsAvailableOnDevice();
|
||||
const isKeystoreAvailable = await NativeVaultManager.isKeystoreAvailable();
|
||||
|
||||
/*
|
||||
|
||||
@@ -4,6 +4,8 @@ import { router } from 'expo-router';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { StyleSheet, View, KeyboardAvoidingView, Platform, ScrollView, Dimensions, Text } from 'react-native';
|
||||
|
||||
import { AppUnlockUtility } from '@/utils/AppUnlockUtility';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { AppErrorCode, getAppErrorCode, getErrorTranslationKey, formatErrorWithCode } from '@/utils/types/errors/AppErrorCodes';
|
||||
import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError';
|
||||
|
||||
@@ -25,7 +27,7 @@ import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
* Unlock screen.
|
||||
*/
|
||||
export default function UnlockScreen() : React.ReactNode {
|
||||
const { isLoggedIn, username, isBiometricsEnabled, getBiometricDisplayName, getEncryptionKeyDerivationParams } = useApp();
|
||||
const { isLoggedIn, username, getEncryptionKeyDerivationParams } = useApp();
|
||||
const { logoutUserInitiated, logoutForced } = useLogout();
|
||||
const dbContext = useDb();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -74,13 +76,13 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Check if biometrics is available
|
||||
const enabled = await isBiometricsEnabled();
|
||||
const enabled = await AppUnlockUtility.isBiometricUnlockAvailable();
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
setIsBiometricsAvailable(enabled);
|
||||
|
||||
const displayName = await getBiometricDisplayName();
|
||||
const displayName = await AppUnlockUtility.getBiometricDisplayName();
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
@@ -124,9 +126,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
@@ -135,13 +135,18 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if max attempts reached
|
||||
if (err && typeof err === 'object' && 'code' in err && err.code === 'MAX_ATTEMPTS_REACHED') {
|
||||
// Max attempts reached - vault has been cleared, force logout
|
||||
await logoutForced();
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('Unlock error:', err);
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
if (!errorCode || errorCode === AppErrorCode.VAULT_DECRYPT_FAILED) {
|
||||
setError(t('auth.errors.incorrectPassword'));
|
||||
@@ -162,7 +167,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
return (): void => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [isBiometricsEnabled, getKeyDerivationParams, getBiometricDisplayName, dbContext, isLoggedIn, username, t, logoutForced]);
|
||||
}, [getKeyDerivationParams, dbContext, isLoggedIn, username, t, logoutForced]);
|
||||
|
||||
/**
|
||||
* Hide the alert dialog.
|
||||
@@ -204,9 +209,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
/*
|
||||
* Navigate to reinitialize which will sync vault with server
|
||||
@@ -220,13 +223,18 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if max attempts reached
|
||||
if (err && typeof err === 'object' && 'code' in err && err.code === 'MAX_ATTEMPTS_REACHED') {
|
||||
// Max attempts reached - vault has been cleared, force logout
|
||||
await logoutForced();
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to extract error code from the error
|
||||
const errorCode = getAppErrorCode(err);
|
||||
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
/*
|
||||
* During unlock, VAULT_DECRYPT_FAILED indicates wrong password.
|
||||
@@ -263,9 +271,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
return true;
|
||||
@@ -301,9 +307,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
}
|
||||
|
||||
// Haptic feedback for successful unlock
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
router.replace('/reinitialize');
|
||||
} catch (err) {
|
||||
@@ -321,9 +325,7 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
await performPinUnlock();
|
||||
} else if (errorCode) {
|
||||
// Haptic feedback for authentication error
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Error);
|
||||
|
||||
// Show the error with code if no PIN fallback
|
||||
const translationKey = getErrorTranslationKey(errorCode);
|
||||
@@ -403,15 +405,19 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
minHeight: 50,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
buttonText: {
|
||||
color: colors.primarySurfaceText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
@@ -430,14 +436,18 @@ export default function UnlockScreen() : React.ReactNode {
|
||||
},
|
||||
faceIdButton: {
|
||||
alignItems: 'center',
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
minHeight: 50,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
faceIdButtonText: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 4,
|
||||
textAlign: 'center',
|
||||
},
|
||||
gradientContainer: {
|
||||
height: Dimensions.get('window').height * 0.4,
|
||||
|
||||
@@ -187,6 +187,7 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
|
||||
gap: 8,
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.6,
|
||||
@@ -194,6 +195,8 @@ export const ConfirmDialog: React.FC<IConfirmDialogProps> = ({
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { ModalWrapper } from '@/components/common/ModalWrapper';
|
||||
|
||||
@@ -62,9 +63,7 @@ export const FolderModal: React.FC<IFolderModalProps> = ({
|
||||
await onSave(trimmedName);
|
||||
|
||||
// Haptic feedback for successful folder creation/edit
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||
}
|
||||
HapticsUtility.notification(Haptics.NotificationFeedbackType.Success);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { forwardRef, useImperativeHandle, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, TouchableOpacity, Platform, ScrollView, Switch } from 'react-native';
|
||||
import { View, TextInput, TextInputProps, StyleSheet, Platform, TouchableOpacity, ScrollView, Switch } from 'react-native';
|
||||
|
||||
import type { PasswordSettings } from '@/utils/dist/core/models/vault';
|
||||
import { CreatePasswordGenerator } from '@/utils/dist/core/password-generator';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
import { sliderToLength, lengthToSlider, SLIDER_MIN, SLIDER_MAX } from '@/utils/passwordLengthSlider';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
@@ -145,9 +145,7 @@ const AdvancedPasswordFieldComponent = forwardRef<AdvancedPasswordFieldRef, Adva
|
||||
setShowPasswordState(true);
|
||||
|
||||
// Haptic feedback for password generation
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
}
|
||||
}
|
||||
}, [currentSettings, generatePassword, onChangeText, setShowPasswordState]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import DraggableFlatList, {
|
||||
|
||||
import type { FieldType } from '@/utils/dist/core/models/vault';
|
||||
import { FieldTypes } from '@/utils/dist/core/models/vault';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -162,7 +163,7 @@ export const DraggableCustomFieldsList: React.FC<DraggableCustomFieldsListProps>
|
||||
* Handle drag begin
|
||||
*/
|
||||
const handleDragBegin = useCallback(() => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||
HapticsUtility.impact(Haptics.ImpactFeedbackStyle.Medium);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, Easing } from 'react-native';
|
||||
import { View, Text, Platform, TouchableOpacity, StyleSheet, Animated, Easing } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
|
||||
import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility';
|
||||
import { HapticsUtility } from '@/utils/HapticsUtility';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
@@ -107,9 +107,7 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
await copyToClipboardWithExpiration(value, timeoutSeconds);
|
||||
|
||||
// Haptic feedback for successful copy
|
||||
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
}
|
||||
HapticsUtility.impact();
|
||||
|
||||
// Handle animation state
|
||||
if (timeoutSeconds > 0) {
|
||||
|
||||