Merge pull request #964 from orange-elephant/google-auth-style-export

Google Authenticator compatible export
This commit is contained in:
Alexander Bakker 2022-09-17 15:46:56 +02:00 committed by GitHub
commit 66b7fd38d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 173 additions and 121 deletions

View file

@ -9,6 +9,7 @@ import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
@ -18,7 +19,7 @@ import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class GoogleAuthInfo implements Serializable {
public class GoogleAuthInfo implements Transferable, Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
@ -267,6 +268,7 @@ public class GoogleAuthInfo implements Serializable {
return _info;
}
@Override
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
@ -319,7 +321,7 @@ public class GoogleAuthInfo implements Serializable {
return _accountName;
}
public static class Export {
public static class Export implements Transferable, Serializable {
private int _batchId;
private int _batchIndex;
private int _batchSize;
@ -385,5 +387,72 @@ public class GoogleAuthInfo implements Serializable {
return true;
}
@Override
public Uri getUri() throws GoogleAuthInfoException {
GoogleAuthProtos.MigrationPayload.Builder builder = GoogleAuthProtos.MigrationPayload.newBuilder();
builder.setBatchId(_batchId)
.setBatchIndex(_batchIndex)
.setBatchSize(_batchSize)
.setVersion(1);
for (GoogleAuthInfo info: _entries) {
GoogleAuthProtos.MigrationPayload.OtpParameters.Builder parameters = GoogleAuthProtos.MigrationPayload.OtpParameters.newBuilder()
.setSecret(ByteString.copyFrom(info.getOtpInfo().getSecret()))
.setName(info.getAccountName())
.setIssuer(info.getIssuer());
switch (info.getOtpInfo().getAlgorithm(false)) {
case "SHA1":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA1);
break;
case "SHA256":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA256);
break;
case "SHA512":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA512);
break;
case "MD5":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_MD5);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported Algorithm: %s", info.getOtpInfo().getAlgorithm(false)));
}
switch (info.getOtpInfo().getDigits()) {
case 6:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_SIX);
break;
case 8:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported number of digits: %s", info.getOtpInfo().getDigits()));
}
switch (info.getOtpInfo().getType().toLowerCase()) {
case HotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_HOTP);
parameters.setCounter(((HotpInfo) info.getOtpInfo()).getCounter());
break;
case TotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_TOTP);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Type unsupported by GoogleAuthProtos: %s", info.getOtpInfo().getType()));
}
builder.addOtpParameters(parameters.build());
}
Uri.Builder exportUriBuilder = new Uri.Builder()
.scheme(SCHEME_EXPORT)
.authority("offline");
String data = Base64.encode(builder.build().toByteArray());
exportUriBuilder.appendQueryParameter("data", data);
return exportUriBuilder.build();
}
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public interface Transferable {
Uri getUri() throws GoogleAuthInfoException;
}

View file

@ -17,6 +17,8 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.Transferable;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.google.zxing.WriterException;
@ -24,8 +26,9 @@ import java.util.ArrayList;
import java.util.List;
public class TransferEntriesActivity extends AegisActivity {
private List<GoogleAuthInfo> _authInfos;
private List<Transferable> _authInfos;
private ImageView _qrImage;
private TextView _description;
private TextView _issuer;
private TextView _accountName;
private TextView _entriesCount;
@ -43,6 +46,7 @@ public class TransferEntriesActivity extends AegisActivity {
setSupportActionBar(findViewById(R.id.toolbar));
_qrImage = findViewById(R.id.ivQrCode);
_description = findViewById(R.id.tvDescription);
_issuer = findViewById(R.id.tvIssuer);
_accountName = findViewById(R.id.tvAccountName);
_entriesCount = findViewById(R.id.tvEntriesCount);
@ -55,7 +59,7 @@ public class TransferEntriesActivity extends AegisActivity {
}
Intent intent = getIntent();
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
_authInfos = (ArrayList<Transferable>) intent.getSerializableExtra("authInfos");
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
_nextButton.setVisibility(controlVisibility);
@ -103,10 +107,16 @@ public class TransferEntriesActivity extends AegisActivity {
}
private void generateQR() {
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
_issuer.setText(selectedEntry.getIssuer());
_accountName.setText(selectedEntry.getAccountName());
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
Transferable selectedEntry = _authInfos.get(_currentEntryCount - 1);
if (selectedEntry instanceof GoogleAuthInfo) {
GoogleAuthInfo entry = (GoogleAuthInfo) selectedEntry;
_issuer.setText(entry.getIssuer());
_accountName.setText(entry.getAccountName());
} else if (selectedEntry instanceof GoogleAuthInfo.Export) {
_description.setText(R.string.google_auth_compatible_transfer_description);
}
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
@ -118,7 +128,7 @@ public class TransferEntriesActivity extends AegisActivity {
Bitmap bitmap;
try {
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), 512, 512, backgroundColor);
} catch (WriterException e) {
} catch (WriterException | GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}

View file

@ -22,11 +22,17 @@ import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
import com.beemdevelopment.aegis.ui.TransferEntriesActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
@ -37,6 +43,10 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import javax.crypto.Cipher;
@ -78,6 +88,12 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
startExport();
return true;
});
Preference googleAuthStyleExportPreference = requirePreference("pref_google_auth_style_export");
googleAuthStyleExportPreference.setOnPreferenceClickListener(preference -> {
startGoogleAuthenticatorStyleExport();
return true;
});
}
@Override
@ -231,6 +247,52 @@ public class ImportExportPreferencesFragment extends PreferencesFragment {
Dialogs.showSecureDialog(dialog);
}
private void startGoogleAuthenticatorStyleExport() {
ArrayList<GoogleAuthInfo> toExport = new ArrayList<>();
for (VaultEntry entry : _vaultManager.getVault().getEntries()) {
String type = entry.getInfo().getType().toLowerCase();
String algo = entry.getInfo().getAlgorithm(false);
int digits = entry.getInfo().getDigits();
if ((Objects.equals(type, TotpInfo.ID) || Objects.equals(type, HotpInfo.ID))
&& digits == OtpInfo.DEFAULT_DIGITS
&& Objects.equals(algo, OtpInfo.DEFAULT_ALGORITHM)) {
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
toExport.add(info);
}
}
int entriesSkipped = _vaultManager.getVault().getEntries().size() - toExport.size();
if (entriesSkipped > 0) {
Toast a = new Toast(requireContext());
a.setText(requireContext().getResources().getQuantityString(R.plurals.pref_google_auth_export_incompatible_entries, entriesSkipped, entriesSkipped));
a.show();
}
int qrSize = 10;
int batchId = new Random().nextInt();
int batchSize = toExport.size() / qrSize + (toExport.size() % qrSize == 0 ? 0 : 1);
List<GoogleAuthInfo> infos = new ArrayList<>();
ArrayList<GoogleAuthInfo.Export> exports = new ArrayList<>();
for (int i = 0, batchIndex = 0; i < toExport.size(); i++) {
infos.add(toExport.get(i));
if (infos.size() == qrSize || toExport.size() == i + 1) {
exports.add(new GoogleAuthInfo.Export(infos, batchId, batchIndex++, batchSize));
infos = new ArrayList<>();
}
}
if (exports.size() == 0) {
Toast t = new Toast(requireContext());
t.setText(R.string.pref_google_auth_export_no_data);
t.show();
} else {
Intent intent = new Intent(requireContext(), TransferEntriesActivity.class);
intent.putExtra("authInfos", exports);
startActivity(intent);
}
}
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;

View file

@ -266,10 +266,6 @@
<string name="empty_group_list">Няма групи за показване. Добавете групи в екрана за редактиране на запис</string>
<string name="empty_group_list_title">Няма намерени групи</string>
<string name="done">Готово</string>
<plurals name="entries_count">
<item quantity="one">%d / %d запис</item>
<item quantity="other">%d / %d записи</item>
</plurals>
<string name="next">Следващ</string>
<string name="previous">Предишен</string>
<string name="transfer_entry">Прехвърляне на запис</string>

View file

@ -268,12 +268,6 @@
<string name="empty_group_list">Žádné skupiny k zobrazení. Skupiny můžete přidat z obrazovky úprav položky.</string>
<string name="empty_group_list_title">Nenalezeny žádné skupiny</string>
<string name="done">Hotovo</string>
<plurals name="entries_count">
<item quantity="one">%d/%d položka</item>
<item quantity="few">%d/%d položek</item>
<item quantity="many">%d/%d položek</item>
<item quantity="other">%d/%d položek</item>
</plurals>
<string name="next">Další</string>
<string name="previous">Předchozí</string>
<string name="transfer_entry">Přenést položku</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Vælg et ikon</string>
<string name="uncategorized">Ukategoriseret</string>
<string name="done">Færdig</string>
<plurals name="entries_count">
<item quantity="one">%d / %d post</item>
<item quantity="other">%d / %d poster</item>
</plurals>
<string name="next">Næste</string>
<string name="previous">Forrige</string>
<string name="transfer_entry">Overfør post</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Wähle ein Symbol aus</string>
<string name="uncategorized">Nicht kategorisiert</string>
<string name="done">Fertig</string>
<plurals name="entries_count">
<item quantity="one">1 Eintrag</item>
<item quantity="other">%d / %d Einträge</item>
</plurals>
<string name="next">Weiter</string>
<string name="previous">Zurück</string>
<string name="transfer_entry">Eintrag übertragen</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Επιλέξτε ένα εικονίδιο</string>
<string name="uncategorized">Μη κατηγοριοποιημένο</string>
<string name="done">Ολοκληρώθηκε</string>
<plurals name="entries_count">
<item quantity="one">%d / %d καταχώρηση</item>
<item quantity="other">%d / %d καταχωρήσεις</item>
</plurals>
<string name="next">Επόμενο</string>
<string name="previous">Προηγούμενο</string>
<string name="transfer_entry">Μεταφορά καταχώρησης</string>

View file

@ -294,10 +294,6 @@
<string name="pick_icon">Seleccione un icono</string>
<string name="uncategorized">Sin categorizar</string>
<string name="done">Hecho</string>
<plurals name="entries_count">
<item quantity="one">%d / %d entrada</item>
<item quantity="other">%d / %d entradas</item>
</plurals>
<string name="next">Siguiente</string>
<string name="previous">Anterior</string>
<string name="transfer_entry">Transferir entrada</string>

View file

@ -291,10 +291,6 @@
<string name="pick_icon">Aukeratu ikono bat</string>
<string name="uncategorized">Kategoriarik gabe</string>
<string name="done">Eginda</string>
<plurals name="entries_count">
<item quantity="one">%d / %d sarrera</item>
<item quantity="other">%d / %d sarrera</item>
</plurals>
<string name="next">Hurrengoa</string>
<string name="previous">Aurrekoa</string>
<string name="transfer_entry">Transferitu sarrera</string>

View file

@ -257,10 +257,6 @@
<string name="empty_group_list">هیچ گروهی پیدا نشد. شما می‌توانید در صفحه ویرایش هر آیتم، گروه اضافه کنید.</string>
<string name="empty_group_list_title">گروهی پیدا نشد</string>
<string name="done">انجام شد</string>
<plurals name="entries_count">
<item quantity="one">%d / %d آیتم</item>
<item quantity="other">%d / %d آیتم</item>
</plurals>
<string name="next">بعدی</string>
<string name="previous">قبلی</string>
<string name="transfer_entry">انتقال آیتم‌ها</string>

View file

@ -281,10 +281,6 @@
<string name="pick_icon">Valitse kuvake</string>
<string name="uncategorized">Luokittelematon</string>
<string name="done">Valmis</string>
<plurals name="entries_count">
<item quantity="one">%d / %d kohde</item>
<item quantity="other">%d / %d kohdetta</item>
</plurals>
<string name="next">Seuraava</string>
<string name="previous">Edellinen</string>
<string name="transfer_entry">Siirrä kohde</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Choisissez une icône</string>
<string name="uncategorized">Non catégorisé</string>
<string name="done">Fait</string>
<plurals name="entries_count">
<item quantity="one">%d / %d entrée</item>
<item quantity="other">%d / %d entrées</item>
</plurals>
<string name="next">Suivant</string>
<string name="previous">Précédent</string>
<string name="transfer_entry">Transférer une entrée</string>

View file

@ -223,10 +223,6 @@
<string name="empty_group_list">दिखाए जाने के लिये कोई समूह नहीं हैं। एक प्रविष्टि के संपादन स्क्रीन में समूह जोड़ें</string>
<string name="empty_group_list_title">कोई समूह नहीं मिले</string>
<string name="done">हो गया</string>
<plurals name="entries_count">
<item quantity="one">%d / %d प्रविष्टि</item>
<item quantity="other">%d / %d प्रविष्टियाँ</item>
</plurals>
<string name="next">अगला</string>
<string name="previous">पिछला</string>
<string name="transfer_entry">प्रविष्टि स्थानांतरित करें</string>

View file

@ -285,9 +285,6 @@
<string name="pick_icon">Pilih sebuah ikon</string>
<string name="uncategorized">Tidak Dikategorikan</string>
<string name="done">Selesai</string>
<plurals name="entries_count">
<item quantity="other">%d / %d entri</item>
</plurals>
<string name="next">Berikutnya</string>
<string name="previous">Sebelumnya</string>
<string name="transfer_entry">Pindahkan entri</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Scegli un\'icona</string>
<string name="uncategorized">Senza categoria</string>
<string name="done">Fine</string>
<plurals name="entries_count">
<item quantity="one">%d / %d voce</item>
<item quantity="other">%d / %d voci</item>
</plurals>
<string name="next">Avanti</string>
<string name="previous">Indietro</string>
<string name="transfer_entry">Trasferisci voce</string>

View file

@ -286,9 +286,6 @@
<string name="pick_icon">アイコンを選択</string>
<string name="uncategorized">未分類</string>
<string name="done">完了</string>
<plurals name="entries_count">
<item quantity="other">%d / %d エントリー</item>
</plurals>
<string name="next">次へ</string>
<string name="previous">前へ</string>
<string name="transfer_entry">エントリーを転送</string>

View file

@ -301,11 +301,6 @@
<string name="pick_icon">Izvēlēties ikonu</string>
<string name="uncategorized">Bez kopas</string>
<string name="done">Izpildīts</string>
<plurals name="entries_count">
<item quantity="zero">%d / %d ierakstiem</item>
<item quantity="one">%d / %d ierakstiem</item>
<item quantity="other">%d / %d ierakstiem</item>
</plurals>
<string name="next">Nākamais</string>
<string name="previous">Iepriekšējais</string>
<string name="transfer_entry">Pārvietot ierakstu</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Kies een icoon</string>
<string name="uncategorized">Ongecategoriseerd</string>
<string name="done">Klaar</string>
<plurals name="entries_count">
<item quantity="one">%d / %d item</item>
<item quantity="other">%d / %d items</item>
</plurals>
<string name="next">Volgende</string>
<string name="previous">Vorige</string>
<string name="transfer_entry">Item overzetten</string>

View file

@ -298,12 +298,6 @@
<string name="pick_icon">Wybierz ikonę</string>
<string name="uncategorized">Bez kategorii</string>
<string name="done">Gotowe</string>
<plurals name="entries_count">
<item quantity="one">Wpis %d / %d</item>
<item quantity="few">Wpis %d / %d</item>
<item quantity="many">Wpis %d / %d</item>
<item quantity="other">Wpis %d / %d</item>
</plurals>
<string name="next">Następny</string>
<string name="previous">Poprzedni</string>
<string name="transfer_entry">Przenieś wpis</string>

View file

@ -295,10 +295,6 @@
<string name="pick_icon">Escolha um ícone</string>
<string name="uncategorized">Sem Categoria</string>
<string name="done">Terminado</string>
<plurals name="entries_count">
<item quantity="one">%d / %d entrada</item>
<item quantity="other">%d / %d entradas</item>
</plurals>
<string name="next">Próximo</string>
<string name="previous">Anterior</string>
<string name="transfer_entry">Transferir entrada</string>

View file

@ -232,10 +232,6 @@
<string name="empty_group_list">Não há grupos a serem mostrados. Adicione grupos na tela de edição de uma entrada</string>
<string name="empty_group_list_title">Nenhum grupo encontrado</string>
<string name="done">Concluído</string>
<plurals name="entries_count">
<item quantity="one">%d / %d entrada</item>
<item quantity="other">%d / %d entradas</item>
</plurals>
<string name="next">Próximo</string>
<string name="previous">Anterior</string>
<string name="transfer_entry">Transferir entrada</string>

View file

@ -301,11 +301,6 @@
<string name="pick_icon">Alege o pictogramă</string>
<string name="uncategorized">Necategorizat</string>
<string name="done">Realizat</string>
<plurals name="entries_count">
<item quantity="one">%d / %d intrare</item>
<item quantity="few">%d / %d intrări</item>
<item quantity="other">%d / %d intrări</item>
</plurals>
<string name="next">Următorul</string>
<string name="previous">Anteriorul</string>
<string name="transfer_entry">Transfer intrare</string>

View file

@ -307,12 +307,6 @@
<string name="pick_icon">Выбрать значок</string>
<string name="uncategorized">Без категории</string>
<string name="done">Готово</string>
<plurals name="entries_count">
<item quantity="one">%d / %d запись</item>
<item quantity="few">%d / %d записи</item>
<item quantity="many">%d / %d записей</item>
<item quantity="other">%d / %d записей</item>
</plurals>
<string name="next">Следующий</string>
<string name="previous">Предыдущий</string>
<string name="transfer_entry">Передача записи</string>

View file

@ -293,10 +293,6 @@
<string name="pick_icon">Bir ikon seçin</string>
<string name="uncategorized">Kategorisiz</string>
<string name="done">Tamamlandı</string>
<plurals name="entries_count">
<item quantity="one">%d / %d girişi</item>
<item quantity="other">%d / %d girişleri</item>
</plurals>
<string name="next">Sonraki</string>
<string name="previous">Önceki</string>
<string name="transfer_entry">Girdiyi aktar</string>

View file

@ -276,12 +276,6 @@
<string name="empty_group_list">Немає груп для показу. Додайте групи на екрані редагування запису</string>
<string name="empty_group_list_title">Групи не знайдено</string>
<string name="done">Готово</string>
<plurals name="entries_count">
<item quantity="one">%d / %d запис</item>
<item quantity="few">%d / %d записів</item>
<item quantity="many">%d / %d записів</item>
<item quantity="other">%d / %d записів</item>
</plurals>
<string name="next">Наступний</string>
<string name="previous">Попередній</string>
<string name="transfer_entry">Передати запис</string>

View file

@ -289,9 +289,6 @@
<string name="pick_icon">Chọn biểu tượng</string>
<string name="uncategorized">Chưa được phân loại</string>
<string name="done">Xong</string>
<plurals name="entries_count">
<item quantity="other">%d / %d mục</item>
</plurals>
<string name="next">Tiếp</string>
<string name="previous">Trước</string>
<string name="transfer_entry">Truyền mục</string>

View file

@ -289,9 +289,6 @@
<string name="pick_icon">选择一个图标</string>
<string name="uncategorized">未分类</string>
<string name="done">完成</string>
<plurals name="entries_count">
<item quantity="other">%d / %d 项条目</item>
</plurals>
<string name="next">下一个</string>
<string name="previous">上一个</string>
<string name="transfer_entry">迁移条目</string>

View file

@ -250,9 +250,6 @@
<string name="empty_group_list">沒有可以顯示的群組,請先在編輯頁面增加一個群組</string>
<string name="empty_group_list_title">找不到任何群組</string>
<string name="done">完成</string>
<plurals name="entries_count">
<item quantity="other">%d / %d 條目</item>
</plurals>
<string name="next">下一個</string>
<string name="previous">上一個</string>
<string name="transfer_entry">轉移條目</string>

View file

@ -63,6 +63,13 @@
<string name="pref_export_title">Export</string>
<string name="pref_export_summary">Export the vault</string>
<string name="pref_password_reminder_title">Password reminder</string>
<string name="pref_google_auth_export_title">Export for Google Authenticator</string>
<string name="pref_google_auth_export_descriptor">Generates export QR codes compatible with Google Authenticator</string>
<string name="pref_google_auth_export_no_data">No data to export</string>
<plurals name="pref_google_auth_export_incompatible_entries">
<item quantity="one">Skipped %d incompatible entry</item>
<item quantity="other">Skipped %d incompatible entries</item>
</plurals>
<string name="pref_password_reminder_summary">Show a %s reminder to enter the password, so that you don\'t forget it.</string>
<string name="pref_password_reminder_summary_disabled">Disabled</string>
<string name="pref_secure_screen_title">Screen security</string>
@ -368,14 +375,15 @@
<string name="pick_icon">Pick an icon</string>
<string name="uncategorized">Uncategorized</string>
<string name="done">Done</string>
<plurals name="entries_count">
<item quantity="one">%d / %d entry</item>
<item quantity="other">%d / %d entries</item>
<plurals name="qr_count">
<item quantity="one">%d / %d QR code</item>
<item quantity="other">%d / %d QR codes</item>
</plurals>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="transfer_entry">Transfer entry</string>
<string name="transfer_entry_description">Scan this QR code with the authenticator app you would like to transfer this entry to</string>
<string name="google_auth_compatible_transfer_description">Scan these QR codes with Aegis or Google Authenticator.\n\nDue to limitations of the Google Authenticator app, only TOTP &amp; HOTP tokens that use SHA1 and produce 6-digit codes are included</string>
<string name="password_strength_very_weak">Very weak</string>
<string name="password_strength_weak">Weak</string>

View file

@ -17,4 +17,9 @@
android:title="@string/pref_export_title"
android:summary="@string/pref_export_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_google_auth_style_export"
android:title="@string/pref_google_auth_export_title"
android:summary="@string/pref_google_auth_export_descriptor"
app:iconSpaceReserved="false"/>
</PreferenceScreen>