fix(security): require a fresh PIN or biometric check for sensitive actions

An unlocked phone was enough to read the 24-word recovery phrase. The
phrase, backup export and import, the duress PIN, a longer auto-lock,
turning off screen security, enabling biometrics and deleting the
account now ask for the PIN (or biometrics with biometricOnly) every
time. The duress session never shows the phrase. PIN checks outside the
lock screen count towards the same failure throttle.
This commit is contained in:
Julius Joska
2026-10-23 10:50:16 +02:00
parent 5b237c82f2
commit 2f6b542f24
19 changed files with 1442 additions and 45 deletions
@@ -3025,6 +3025,162 @@ abstract class AppLocalizations {
/// In cs, this message translates to:
/// **'Fotku se nepodařilo nastavit: {error}'**
String profilePhotoFailed(String error);
/// Nadpis výzvy k PINu před citlivou akcí v odemčené appce
///
/// In cs, this message translates to:
/// **'Potvrď, že jsi to ty'**
String get reauthTitle;
/// Snackbar: citlivá akce bez nastaveného PINu
///
/// In cs, this message translates to:
/// **'Tahle akce potřebuje PIN. Nejdřív si ho nastav.'**
String get reauthPinRequired;
/// Akce snackbaru: nastavit PIN
///
/// In cs, this message translates to:
/// **'Nastavit PIN'**
String get reauthSetPin;
/// Důvod ve výzvě k PINu/biometrii: zobrazení obnovovací fráze
///
/// In cs, this message translates to:
/// **'Zobrazení 24 slov pro obnovu účtu'**
String get reauthReasonSeed;
/// Důvod: export zálohy
///
/// In cs, this message translates to:
/// **'Export zálohy'**
String get reauthReasonBackupExport;
/// Důvod: obnova ze zálohy
///
/// In cs, this message translates to:
/// **'Obnova ze zálohy'**
String get reauthReasonBackupImport;
/// Důvod: nastavení nebo zrušení nátlakového PINu
///
/// In cs, this message translates to:
/// **'Změna nátlakového PINu'**
String get reauthReasonDecoyPin;
/// Důvod: prodloužení nebo vypnutí automatického zámku
///
/// In cs, this message translates to:
/// **'Prodloužení automatického zámku'**
String get reauthReasonAutoLock;
/// Důvod: vypnutí ochrany obrazovky
///
/// In cs, this message translates to:
/// **'Vypnutí ochrany obrazovky'**
String get reauthReasonScreenSecurity;
/// Důvod: smazání účtu
///
/// In cs, this message translates to:
/// **'Smazání účtu a všech dat'**
String get reauthReasonDeleteAccount;
/// Důvod: zapnutí biometrického odemčení
///
/// In cs, this message translates to:
/// **'Zapnutí biometrie'**
String get reauthReasonBiometrics;
/// Jméno neznámého odesílatele (žádost o zprávu) všude v UI; {name} si zvolil sám
///
/// In cs, this message translates to:
/// **'{name} (neznámý)'**
String contactRequestName(String name);
/// Text notifikace pro zprávu od neznámého odesílatele — bez jména, které si zvolil
///
/// In cs, this message translates to:
/// **'Žádost o zprávu od neznámého odesílatele'**
String get pushMessageRequest;
/// Nadpis panelu / štítek žádosti o zprávu
///
/// In cs, this message translates to:
/// **'Žádost o zprávu'**
String get requestTitle;
/// Panel žádosti o zprávu; {name} = jméno, které si odesílatel zvolil
///
/// In cs, this message translates to:
/// **'„{name}“ není ve tvých kontaktech. Jméno i fotku si zvolil sám, takže nemusí jít o toho, za koho se vydává. Dokud ho nepřijmeš, neuvidí, že jsi zprávu četl, a nemůže ti volat.'**
String requestBody(String name);
/// Varování v panelu žádosti: jméno se plete s existujícím kontaktem {name}
///
/// In cs, this message translates to:
/// **'Pozor: stejně se jmenuje tvůj kontakt {name}. Tohle je jiný klíč, tedy jiný telefon nebo jiný člověk. Než odpovíš, ověř si to jinou cestou (zavolej, potkejte se).'**
String requestLookalikeWarning(String name);
/// Tlačítko: přijmout žádost o zprávu
///
/// In cs, this message translates to:
/// **'Přijmout'**
String get requestAccept;
/// Nadpis dialogu přijetí žádosti
///
/// In cs, this message translates to:
/// **'Přijmout jako kontakt'**
String get requestAcceptTitle;
/// Text dialogu přijetí: kontakt pojmenuje uživatel sám
///
/// In cs, this message translates to:
/// **'Pojmenuj kontakt sám. Jméno, které si odesílatel zvolil, se nepřebírá automaticky.'**
String get requestAcceptBody;
/// Popisek pole se jménem kontaktu
///
/// In cs, this message translates to:
/// **'Jméno kontaktu'**
String get requestAcceptNameLabel;
/// Chyba: prázdné jméno
///
/// In cs, this message translates to:
/// **'Zadej jméno.'**
String get requestNameEmpty;
/// Chyba: jméno se plete s jiným kontaktem
///
/// In cs, this message translates to:
/// **'Tak (nebo skoro tak) se už jmenuje jiný kontakt. Zvol jméno, které je nezamění.'**
String get requestNameTaken;
/// Upozornění v konverzaci: kontaktu {name} se změnil identity klíč
///
/// In cs, this message translates to:
/// **'Bezpečnostní kód kontaktu {name} se změnil. Pod jeho adresou se ozval jiný klíč: nový telefon, přeinstalace, nebo podvrh. Ověření je zrušené. Než pošleš něco citlivého, porovnejte kód znovu.'**
String noticeIdentityChanged(String name);
/// Upozornění v konverzaci existujícího kontaktu {name}: neznámý si zvolil podobné jméno {claimed}
///
/// In cs, this message translates to:
/// **'Neznámý odesílatel si dal jméno „{claimed}“, které se plete s kontaktem {name}. Tento kontakt to není, zpráva přišla z jiného klíče. Najdeš ji v chatech jako žádost o zprávu.'**
String noticeLookalikeRequest(String claimed, String name);
/// Tlačítko u upozornění na změnu kódu
///
/// In cs, this message translates to:
/// **'Ověřit kód'**
String get noticeVerifyAction;
/// Náhled v seznamu chatů, když je poslední položkou bezpečnostní upozornění
///
/// In cs, this message translates to:
/// **'Bezpečnostní upozornění'**
String get noticePreview;
}
class _AppLocalizationsDelegate
@@ -1726,4 +1726,95 @@ class AppLocalizationsCs extends AppLocalizations {
String profilePhotoFailed(String error) {
return 'Fotku se nepodařilo nastavit: $error';
}
@override
String get reauthTitle => 'Potvrď, že jsi to ty';
@override
String get reauthPinRequired =>
'Tahle akce potřebuje PIN. Nejdřív si ho nastav.';
@override
String get reauthSetPin => 'Nastavit PIN';
@override
String get reauthReasonSeed => 'Zobrazení 24 slov pro obnovu účtu';
@override
String get reauthReasonBackupExport => 'Export zálohy';
@override
String get reauthReasonBackupImport => 'Obnova ze zálohy';
@override
String get reauthReasonDecoyPin => 'Změna nátlakového PINu';
@override
String get reauthReasonAutoLock => 'Prodloužení automatického zámku';
@override
String get reauthReasonScreenSecurity => 'Vypnutí ochrany obrazovky';
@override
String get reauthReasonDeleteAccount => 'Smazání účtu a všech dat';
@override
String get reauthReasonBiometrics => 'Zapnutí biometrie';
@override
String contactRequestName(String name) {
return '$name (neznámý)';
}
@override
String get pushMessageRequest => 'Žádost o zprávu od neznámého odesílatele';
@override
String get requestTitle => 'Žádost o zprávu';
@override
String requestBody(String name) {
return '„$name“ není ve tvých kontaktech. Jméno i fotku si zvolil sám, takže nemusí jít o toho, za koho se vydává. Dokud ho nepřijmeš, neuvidí, že jsi zprávu četl, a nemůže ti volat.';
}
@override
String requestLookalikeWarning(String name) {
return 'Pozor: stejně se jmenuje tvůj kontakt $name. Tohle je jiný klíč, tedy jiný telefon nebo jiný člověk. Než odpovíš, ověř si to jinou cestou (zavolej, potkejte se).';
}
@override
String get requestAccept => 'Přijmout';
@override
String get requestAcceptTitle => 'Přijmout jako kontakt';
@override
String get requestAcceptBody =>
'Pojmenuj kontakt sám. Jméno, které si odesílatel zvolil, se nepřebírá automaticky.';
@override
String get requestAcceptNameLabel => 'Jméno kontaktu';
@override
String get requestNameEmpty => 'Zadej jméno.';
@override
String get requestNameTaken =>
'Tak (nebo skoro tak) se už jmenuje jiný kontakt. Zvol jméno, které je nezamění.';
@override
String noticeIdentityChanged(String name) {
return 'Bezpečnostní kód kontaktu $name se změnil. Pod jeho adresou se ozval jiný klíč: nový telefon, přeinstalace, nebo podvrh. Ověření je zrušené. Než pošleš něco citlivého, porovnejte kód znovu.';
}
@override
String noticeLookalikeRequest(String claimed, String name) {
return 'Neznámý odesílatel si dal jméno „$claimed“, které se plete s kontaktem $name. Tento kontakt to není, zpráva přišla z jiného klíče. Najdeš ji v chatech jako žádost o zprávu.';
}
@override
String get noticeVerifyAction => 'Ověřit kód';
@override
String get noticePreview => 'Bezpečnostní upozornění';
}
@@ -1748,4 +1748,96 @@ class AppLocalizationsDe extends AppLocalizations {
String profilePhotoFailed(String error) {
return 'Foto konnte nicht gesetzt werden: $error';
}
@override
String get reauthTitle => 'Bestätige, dass du es bist';
@override
String get reauthPinRequired =>
'Diese Aktion erfordert eine PIN. Lege zuerst eine fest.';
@override
String get reauthSetPin => 'PIN festlegen';
@override
String get reauthReasonSeed => '24 Wiederherstellungswörter anzeigen';
@override
String get reauthReasonBackupExport => 'Backup exportieren';
@override
String get reauthReasonBackupImport => 'Aus Backup wiederherstellen';
@override
String get reauthReasonDecoyPin => 'Notfall-PIN ändern';
@override
String get reauthReasonAutoLock => 'Automatische Sperre verlängern';
@override
String get reauthReasonScreenSecurity => 'Bildschirmschutz ausschalten';
@override
String get reauthReasonDeleteAccount => 'Konto und alle Daten löschen';
@override
String get reauthReasonBiometrics => 'Biometrie einschalten';
@override
String contactRequestName(String name) {
return '$name (unbekannt)';
}
@override
String get pushMessageRequest =>
'Nachrichtenanfrage von unbekanntem Absender';
@override
String get requestTitle => 'Nachrichtenanfrage';
@override
String requestBody(String name) {
return '„$name“ ist nicht in deinen Kontakten. Name und Foto hat die Person selbst gewählt, sie ist also vielleicht nicht, wer sie vorgibt zu sein. Bis du annimmst, sieht sie nicht, dass du die Nachricht gelesen hast, und kann dich nicht anrufen.';
}
@override
String requestLookalikeWarning(String name) {
return 'Achtung: Dein Kontakt $name heißt genauso. Das ist ein anderer Schlüssel, also ein anderes Telefon oder eine andere Person. Prüfe das auf anderem Weg, bevor du antwortest (anrufen, persönlich treffen).';
}
@override
String get requestAccept => 'Annehmen';
@override
String get requestAcceptTitle => 'Als Kontakt annehmen';
@override
String get requestAcceptBody =>
'Benenne den Kontakt selbst. Der vom Absender gewählte Name wird nicht automatisch übernommen.';
@override
String get requestAcceptNameLabel => 'Kontaktname';
@override
String get requestNameEmpty => 'Gib einen Namen ein.';
@override
String get requestNameTaken =>
'Ein anderer Kontakt heißt schon so (oder fast so). Wähle einen Namen, der nicht verwechselt werden kann.';
@override
String noticeIdentityChanged(String name) {
return 'Der Sicherheitscode von $name hat sich geändert. Unter dieser Adresse meldet sich ein anderer Schlüssel: neues Telefon, Neuinstallation oder Fälschung. Die Verifizierung wurde aufgehoben. Vergleicht den Code erneut, bevor du etwas Sensibles sendest.';
}
@override
String noticeLookalikeRequest(String claimed, String name) {
return 'Ein unbekannter Absender nennt sich „$claimed“, was nach deinem Kontakt $name aussieht. Es ist nicht dieser Kontakt, die Nachricht kam von einem anderen Schlüssel. Du findest sie in den Chats als Nachrichtenanfrage.';
}
@override
String get noticeVerifyAction => 'Code prüfen';
@override
String get noticePreview => 'Sicherheitshinweis';
}
@@ -1720,4 +1720,94 @@ class AppLocalizationsEn extends AppLocalizations {
String profilePhotoFailed(String error) {
return 'Could not set photo: $error';
}
@override
String get reauthTitle => 'Confirm it\'s you';
@override
String get reauthPinRequired => 'This action needs a PIN. Set one first.';
@override
String get reauthSetPin => 'Set PIN';
@override
String get reauthReasonSeed => 'Show the 24 recovery words';
@override
String get reauthReasonBackupExport => 'Export backup';
@override
String get reauthReasonBackupImport => 'Restore from backup';
@override
String get reauthReasonDecoyPin => 'Change the duress PIN';
@override
String get reauthReasonAutoLock => 'Extend the auto-lock';
@override
String get reauthReasonScreenSecurity => 'Turn off screen security';
@override
String get reauthReasonDeleteAccount => 'Delete the account and all data';
@override
String get reauthReasonBiometrics => 'Turn on biometrics';
@override
String contactRequestName(String name) {
return '$name (unknown)';
}
@override
String get pushMessageRequest => 'Message request from an unknown sender';
@override
String get requestTitle => 'Message request';
@override
String requestBody(String name) {
return '“$name” is not in your contacts. They chose the name and photo themselves, so they may not be who they claim to be. Until you accept, they can\'t see that you read the message and can\'t call you.';
}
@override
String requestLookalikeWarning(String name) {
return 'Careful: your contact $name has the same name. This is a different key, so a different phone or a different person. Check another way before you reply (call, meet in person).';
}
@override
String get requestAccept => 'Accept';
@override
String get requestAcceptTitle => 'Accept as contact';
@override
String get requestAcceptBody =>
'Name the contact yourself. The name the sender chose is not taken over automatically.';
@override
String get requestAcceptNameLabel => 'Contact name';
@override
String get requestNameEmpty => 'Enter a name.';
@override
String get requestNameTaken =>
'Another contact already has this (or a very similar) name. Choose one that can\'t be confused.';
@override
String noticeIdentityChanged(String name) {
return 'The safety code of $name has changed. A different key showed up at their address: a new phone, a reinstall, or an impostor. Verification has been removed. Compare the code again before you send anything sensitive.';
}
@override
String noticeLookalikeRequest(String claimed, String name) {
return 'An unknown sender named themselves “$claimed”, which looks like your contact $name. It is not this contact, the message came from a different key. You\'ll find it in your chats as a message request.';
}
@override
String get noticeVerifyAction => 'Verify code';
@override
String get noticePreview => 'Security notice';
}
@@ -1735,4 +1735,96 @@ class AppLocalizationsIt extends AppLocalizations {
String profilePhotoFailed(String error) {
return 'Impossibile impostare la foto: $error';
}
@override
String get reauthTitle => 'Conferma che sei tu';
@override
String get reauthPinRequired =>
'Questa azione richiede un PIN. Impostalo prima.';
@override
String get reauthSetPin => 'Imposta PIN';
@override
String get reauthReasonSeed => 'Mostra le 24 parole di recupero';
@override
String get reauthReasonBackupExport => 'Esporta backup';
@override
String get reauthReasonBackupImport => 'Ripristina da backup';
@override
String get reauthReasonDecoyPin => 'Modifica il PIN di emergenza';
@override
String get reauthReasonAutoLock => 'Prolunga il blocco automatico';
@override
String get reauthReasonScreenSecurity => 'Disattiva la protezione schermo';
@override
String get reauthReasonDeleteAccount => 'Elimina l\'account e tutti i dati';
@override
String get reauthReasonBiometrics => 'Attiva la biometria';
@override
String contactRequestName(String name) {
return '$name (sconosciuto)';
}
@override
String get pushMessageRequest =>
'Richiesta di messaggio da un mittente sconosciuto';
@override
String get requestTitle => 'Richiesta di messaggio';
@override
String requestBody(String name) {
return '«$name» non è tra i tuoi contatti. Nome e foto li ha scelti da sé, quindi potrebbe non essere chi dice di essere. Finché non accetti, non vede che hai letto il messaggio e non può chiamarti.';
}
@override
String requestLookalikeWarning(String name) {
return 'Attenzione: il tuo contatto $name ha lo stesso nome. Questa è una chiave diversa, quindi un altro telefono o un\'altra persona. Verifica in un altro modo prima di rispondere (chiama, incontratevi).';
}
@override
String get requestAccept => 'Accetta';
@override
String get requestAcceptTitle => 'Accetta come contatto';
@override
String get requestAcceptBody =>
'Dai tu un nome al contatto. Il nome scelto dal mittente non viene ripreso automaticamente.';
@override
String get requestAcceptNameLabel => 'Nome del contatto';
@override
String get requestNameEmpty => 'Inserisci un nome.';
@override
String get requestNameTaken =>
'Un altro contatto ha già questo nome (o quasi). Scegline uno che non si possa confondere.';
@override
String noticeIdentityChanged(String name) {
return 'Il codice di sicurezza di $name è cambiato. Al suo indirizzo si è presentata una chiave diversa: nuovo telefono, reinstallazione o un impostore. La verifica è stata annullata. Confrontate di nuovo il codice prima di inviare qualcosa di sensibile.';
}
@override
String noticeLookalikeRequest(String claimed, String name) {
return 'Un mittente sconosciuto si è chiamato «$claimed», un nome che si confonde con il tuo contatto $name. Non è questo contatto, il messaggio è arrivato da una chiave diversa. Lo trovi nelle chat come richiesta di messaggio.';
}
@override
String get noticeVerifyAction => 'Verifica codice';
@override
String get noticePreview => 'Avviso di sicurezza';
}
@@ -1761,4 +1761,96 @@ class AppLocalizationsRu extends AppLocalizations {
String profilePhotoFailed(String error) {
return 'Не удалось установить фото: $error';
}
@override
String get reauthTitle => 'Подтвердите, что это вы';
@override
String get reauthPinRequired =>
'Для этого действия нужен PIN-код. Сначала задайте его.';
@override
String get reauthSetPin => 'Задать PIN';
@override
String get reauthReasonSeed => 'Показать 24 слова для восстановления';
@override
String get reauthReasonBackupExport => 'Экспорт резервной копии';
@override
String get reauthReasonBackupImport => 'Восстановление из резервной копии';
@override
String get reauthReasonDecoyPin => 'Изменение PIN-кода под принуждением';
@override
String get reauthReasonAutoLock => 'Увеличение времени автоблокировки';
@override
String get reauthReasonScreenSecurity => 'Отключение защиты экрана';
@override
String get reauthReasonDeleteAccount => 'Удаление аккаунта и всех данных';
@override
String get reauthReasonBiometrics => 'Включение биометрии';
@override
String contactRequestName(String name) {
return '$name (неизвестный)';
}
@override
String get pushMessageRequest =>
'Запрос на переписку от неизвестного отправителя';
@override
String get requestTitle => 'Запрос на переписку';
@override
String requestBody(String name) {
return '«$name» нет в ваших контактах. Имя и фото отправитель выбрал сам, поэтому это может быть не тот, за кого он себя выдаёт. Пока вы не примете запрос, он не увидит, что вы прочитали сообщение, и не сможет вам позвонить.';
}
@override
String requestLookalikeWarning(String name) {
return 'Осторожно: так же зовут ваш контакт $name. Это другой ключ, то есть другой телефон или другой человек. Прежде чем отвечать, проверьте это другим способом (позвоните, встретьтесь).';
}
@override
String get requestAccept => 'Принять';
@override
String get requestAcceptTitle => 'Принять как контакт';
@override
String get requestAcceptBody =>
'Назовите контакт сами. Имя, выбранное отправителем, автоматически не используется.';
@override
String get requestAcceptNameLabel => 'Имя контакта';
@override
String get requestNameEmpty => 'Введите имя.';
@override
String get requestNameTaken =>
'Так (или почти так) уже называется другой контакт. Выберите имя, которое нельзя перепутать.';
@override
String noticeIdentityChanged(String name) {
return 'Код безопасности контакта $name изменился. По его адресу появился другой ключ: новый телефон, переустановка или подделка. Подтверждение отменено. Прежде чем отправлять что-то важное, сверьте код заново.';
}
@override
String noticeLookalikeRequest(String claimed, String name) {
return 'Неизвестный отправитель назвался «$claimed», что похоже на ваш контакт $name. Это не он: сообщение пришло с другого ключа. Оно в чатах как запрос на переписку.';
}
@override
String get noticeVerifyAction => 'Проверить код';
@override
String get noticePreview => 'Предупреждение безопасности';
}