Add support for Google Authenticator migration data

This commit is contained in:
MrLetsplay 2023-07-17 20:50:29 +02:00
parent 6c9d6550a4
commit a3ed96c6b0
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
17 changed files with 346 additions and 70 deletions

View File

@ -1,5 +1,6 @@
plugins {
id 'com.android.application'
id 'com.google.protobuf'
}
android {
@ -29,6 +30,32 @@ android {
buildFeatures {
viewBinding true
}
sourceSets {
main {
java {
srcDir "build/generated/source/proto/debug/javalite"
}
proto {
srcDir 'src/main/proto'
}
}
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:21.0-rc-1'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
dependencies {
@ -43,7 +70,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "androidx.biometric:biometric:1.1.0"
implementation 'com.cringe_studios:CringeAuthenticatorLibrary:1.4'
implementation 'com.cringe_studios:CringeAuthenticatorLibrary:1.5'
implementation 'com.google.mlkit:barcode-scanning:17.1.0'
implementation 'com.google.code.gson:gson:2.8.9'
@ -56,4 +83,6 @@ dependencies {
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
implementation 'com.google.protobuf:protobuf-javalite:3.20.1'
}

View File

@ -47,6 +47,7 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
<data android:scheme="otpauth-migration" />
</intent-filter>
</activity>
</application>

View File

@ -30,11 +30,13 @@ import com.cringe_studios.cringe_authenticator.fragment.HomeFragment;
import com.cringe_studios.cringe_authenticator.fragment.MenuFragment;
import com.cringe_studios.cringe_authenticator.fragment.NamedFragment;
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Locale;
@ -58,12 +60,7 @@ public class MainActivity extends AppCompatActivity {
//getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); TODO: enable secure flag
Integer themeID = SettingsUtil.THEMES.get(SettingsUtil.getTheme(this));
if(themeID != null) {
setTheme(themeID);
}else {
setTheme(R.style.Theme_CringeAuthenticator_Blue_Green);
}
ThemeUtil.loadTheme(this);
setLocale(SettingsUtil.getLocale(this));
@ -106,7 +103,7 @@ public class MainActivity extends AppCompatActivity {
Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(fragment instanceof GroupFragment) {
GroupFragment frag = (GroupFragment) fragment;
frag.addOTP(obj.getData());
for(OTPData d : obj.getData()) frag.addOTP(d);
}
});
}
@ -238,7 +235,7 @@ public class MainActivity extends AppCompatActivity {
if(frag instanceof MenuFragment) {
((MenuFragment) frag).addGroup(group);
}
});
}, null);
}
@Override

View File

@ -59,7 +59,7 @@ public class MenuFragment extends NamedFragment {
case 0:
DialogUtil.showCreateGroupDialog(getLayoutInflater(), SettingsUtil.getGroupName(requireContext(), group), newName -> {
renameGroup(group, newName);
});
}, null);
break;
case 1:

View File

@ -1,7 +1,5 @@
package com.cringe_studios.cringe_authenticator.model;
import androidx.annotation.NonNull;
import com.cringe_studios.cringe_authenticator_library.OTP;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPException;
@ -13,6 +11,7 @@ import java.util.Objects;
public class OTPData implements Serializable {
private String name;
private String issuer;
private OTPType type;
private String secret;
private OTPAlgorithm algorithm;
@ -24,8 +23,9 @@ public class OTPData implements Serializable {
// Cached
private transient OTP otp;
public OTPData(String name, OTPType type, String secret, OTPAlgorithm algorithm, int digits, int period, long counter, boolean checksum) {
public OTPData(String name, String issuer, OTPType type, String secret, OTPAlgorithm algorithm, int digits, int period, long counter, boolean checksum) {
this.name = name;
this.issuer = issuer;
this.type = type;
this.secret = secret;
this.algorithm = algorithm;
@ -39,6 +39,10 @@ public class OTPData implements Serializable {
return name;
}
public String getIssuer() {
return issuer;
}
public OTPType getType() {
return type;
}
@ -98,31 +102,17 @@ public class OTPData implements Serializable {
}
}
@NonNull
@Override
public String toString() {
return "OTPData{" +
"name='" + name + '\'' +
", type=" + type +
", secret='" + secret + '\'' +
", algorithm=" + algorithm +
", digits=" + digits +
", period=" + period +
", counter=" + counter +
", checksum=" + checksum +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OTPData otpData = (OTPData) o;
return digits == otpData.digits && period == otpData.period && counter == otpData.counter && Objects.equals(name, otpData.name) && type == otpData.type && Objects.equals(secret, otpData.secret) && algorithm == otpData.algorithm;
return digits == otpData.digits && period == otpData.period && counter == otpData.counter && checksum == otpData.checksum && Objects.equals(name, otpData.name) && Objects.equals(issuer, otpData.issuer) && type == otpData.type && Objects.equals(secret, otpData.secret) && algorithm == otpData.algorithm && Objects.equals(otp, otpData.otp);
}
@Override
public int hashCode() {
return Objects.hash(name, type, secret, algorithm, digits, period, counter);
return Objects.hash(name, issuer, type, secret, algorithm, digits, period, counter, checksum, otp);
}
}

View File

@ -2,6 +2,7 @@ package com.cringe_studios.cringe_authenticator.scanner;
import android.Manifest;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.Image;
@ -27,9 +28,12 @@ import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.ActivityQrScannerBinding;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.util.OTPParser;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.barcode.BarcodeScanner;
import com.google.mlkit.vision.barcode.BarcodeScanning;
@ -48,12 +52,16 @@ public class QRScannerActivity extends AppCompatActivity {
private BarcodeScanner scanner;
private boolean process = true;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
ThemeUtil.loadTheme(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] {Manifest.permission.CAMERA}, 1234);
}
@ -113,6 +121,8 @@ public class QRScannerActivity extends AppCompatActivity {
@OptIn(markerClass = ExperimentalGetImage.class)
@Override
public void analyze(@NonNull ImageProxy image) {
if(!process) return;
Image mediaImage = image.getImage();
if(mediaImage != null) {
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
@ -132,18 +142,40 @@ public class QRScannerActivity extends AppCompatActivity {
if(code == null) return;
Uri uri = Uri.parse(code.getRawValue());
try {
success(OTPParser.parse(uri));
}catch(IllegalArgumentException e) {
error(e.getMessage());
}
process = false;
importUri(uri);
}
}).addOnFailureListener(e -> {});
}
}
}
private void success(@NonNull OTPData data) {
private void importUri(Uri uri) {
if("otpauth-migration".equalsIgnoreCase(uri.getScheme())) {
Dialog dialog = new StyledDialogBuilder(this)
.setTitle(R.string.qr_scanner_migration_title)
.setMessage(R.string.qr_scanner_migration_message)
.setPositiveButton(R.string.yes, (d, which) -> {
try {
success(OTPParser.parseMigration(uri));
}catch(IllegalArgumentException e) {
error(e.getMessage());
}
})
.setNegativeButton(R.string.no, (d, which) -> cancel())
.setOnDismissListener(d -> cancel())
.show();
return;
}
try {
success(OTPParser.parse(uri));
}catch(IllegalArgumentException e) {
error(e.getMessage());
}
}
private void success(@NonNull OTPData... data) {
Intent result = new Intent();
result.putExtra("data", data);
setResult(Activity.RESULT_OK, result);
@ -157,4 +189,9 @@ public class QRScannerActivity extends AppCompatActivity {
finish();
}
private void cancel() {
setResult(RESULT_CANCELED);
finish();
}
}

View File

@ -28,7 +28,7 @@ public class QRScannerContract extends ActivityResultContract<Void, ScannerResul
return null;
}
return new ScannerResult((OTPData) intent.getSerializableExtra("data"));
return new ScannerResult((OTPData[]) intent.getSerializableExtra("data"));
}
}

View File

@ -7,10 +7,10 @@ import com.cringe_studios.cringe_authenticator.model.OTPData;
public class ScannerResult {
private OTPData data;
private OTPData[] data;
private String errorMessage;
public ScannerResult(@NonNull OTPData data) {
public ScannerResult(@NonNull OTPData[] data) {
this.data = data;
}
@ -22,7 +22,7 @@ public class ScannerResult {
return data != null;
}
public OTPData getData() {
public OTPData[] getData() {
return data;
}

View File

@ -1,8 +1,8 @@
package com.cringe_studios.cringe_authenticator.urihandler;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@ -10,11 +10,11 @@ import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPParser;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import java.util.List;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
public class URIHandlerActivity extends AppCompatActivity {
@ -22,32 +22,29 @@ public class URIHandlerActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.loadTheme(this);
Intent intent = getIntent();
if(intent == null) {
finish();
return;
}
Uri uri = intent.getData();
switch(uri.getScheme().toLowerCase()) {
case "otpauth":
importCode(uri);
break;
case "otpauth-migration":
importMigration(uri);
break;
}
}
private void importCode(Uri uri) {
try {
OTPData data = OTPParser.parse(intent.getData());
List<String> groups = SettingsUtil.getGroups(this);
String[] groupNames = new String[groups.size()];
for(int i = 0; i < groups.size(); i++) {
groupNames[i] = SettingsUtil.getGroupName(this, groups.get(i));
}
// TODO: add option to create new group?
AlertDialog dialog = new StyledDialogBuilder(this)
.setTitle(R.string.uri_handler_add_code_title)
.setItems(groupNames, (d, which) -> {
SettingsUtil.addOTP(this, groups.get(which), data);
Toast.makeText(this, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show();
})
.setPositiveButton(R.string.ok, (d, which) -> finish())
.create();
dialog.setOnDismissListener(d -> finish());
dialog.show();
OTPData data = OTPParser.parse(uri);
importCodes(data);
}catch(IllegalArgumentException e) {
AlertDialog dialog = new StyledDialogBuilder(this)
.setTitle(R.string.uri_handler_failed_title)
@ -60,4 +57,26 @@ public class URIHandlerActivity extends AppCompatActivity {
}
}
private void importMigration(Uri uri) {
try {
OTPData[] data = OTPParser.parseMigration(uri);
importCodes(data);
}catch(IllegalArgumentException e) {
AlertDialog dialog = new StyledDialogBuilder(this)
.setTitle(R.string.uri_handler_failed_title)
.setMessage(e.getMessage())
.setPositiveButton(R.string.ok, (d, which) -> finish())
.create();
dialog.setOnDismissListener(d -> finish());
dialog.show();
}
}
private void importCodes(OTPData... data) {
DialogUtil.showImportCodeDialog(this, group -> {
for(OTPData d : data) SettingsUtil.addOTP(this, group, d);
}, this::finish);
}
}

View File

@ -5,6 +5,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@ -19,6 +20,8 @@ import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class DialogUtil {
@ -81,13 +84,23 @@ public class DialogUtil {
showCodeDialog(context, binding.getRoot(), () -> {
try {
String name = binding.inputName.getText().toString();
if(name.trim().isEmpty()) {
showErrorDialog(context, context.getString(R.string.otp_add_missing_name));
return false;
}
String issuer = binding.inputIssuer.getText().toString();
if(issuer.trim().isEmpty()) {
issuer = null;
}
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int period = Integer.parseInt(binding.inputPeriod.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, period, 0, checksum);
OTPData data = new OTPData(name, issuer, OTPType.TOTP, secret, algorithm, digits, period, 0, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
@ -134,13 +147,23 @@ public class DialogUtil {
showCodeDialog(context, binding.getRoot(), () -> {
try {
String name = binding.inputName.getText().toString();
if(name.trim().isEmpty()) {
showErrorDialog(context, context.getString(R.string.otp_add_missing_name));
return false;
}
String issuer = binding.inputIssuer.getText().toString();
if(issuer.trim().isEmpty()) {
issuer = null;
}
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int counter = Integer.parseInt(binding.inputCounter.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, 0, counter, checksum);
OTPData data = new OTPData(name, issuer, OTPType.TOTP, secret, algorithm, digits, 0, counter, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
@ -171,7 +194,7 @@ public class DialogUtil {
}
}
public static void showCreateGroupDialog(LayoutInflater inflater, String initialName, Consumer<String> callback) {
public static void showCreateGroupDialog(LayoutInflater inflater, String initialName, Consumer<String> callback, Runnable onDismiss) {
Context context = inflater.getContext();
DialogCreateGroupBinding binding = DialogCreateGroupBinding.inflate(inflater);
@ -181,7 +204,7 @@ public class DialogUtil {
.setTitle(R.string.action_new_group)
.setView(binding.getRoot())
.setPositiveButton(R.string.add, (view, which) -> {})
.setNegativeButton(R.string.cancel, (view, which) -> {})
.setNegativeButton(R.string.cancel, (view, which) -> { if(onDismiss != null) onDismiss.run(); })
.create();
dialog.setOnShowListener(d -> {
@ -194,9 +217,43 @@ public class DialogUtil {
dialog.dismiss();
callback.accept(binding.createGroupName.getText().toString());
if(onDismiss != null) onDismiss.run();
});
});
dialog.setOnCancelListener(d -> { if(onDismiss != null) onDismiss.run(); });
dialog.show();
}
public static void showImportCodeDialog(Context context, Consumer<String> callback, Runnable onDismiss) {
List<String> groups = SettingsUtil.getGroups(context);
String[] groupNames = new String[groups.size() + 1];
groupNames[0] = context.getString(R.string.uri_handler_create_group);
for(int i = 0; i < groups.size(); i++) {
groupNames[i + 1] = SettingsUtil.getGroupName(context, groups.get(i));
}
AlertDialog dialog = new StyledDialogBuilder(context)
.setTitle(R.string.uri_handler_add_code_title)
.setItems(groupNames, (d, which) -> {
if(which == 0) { // Create New Group
DialogUtil.showCreateGroupDialog(LayoutInflater.from(context), null, group -> {
String id = UUID.randomUUID().toString();
SettingsUtil.addGroup(context, id, group);
callback.accept(id);
}, onDismiss);
return;
}
callback.accept(groups.get(which - 1));
Toast.makeText(context, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show();
if(onDismiss != null) onDismiss.run();
})
.setNegativeButton(R.string.cancel, (d, which) -> { if(onDismiss != null) onDismiss.run(); })
.setOnCancelListener(d -> { if(onDismiss != null) onDismiss.run(); })
.create();
dialog.show();
}

View File

@ -1,20 +1,119 @@
package com.cringe_studios.cringe_authenticator.util;
import android.net.Uri;
import android.util.Base64;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.proto.OTPMigration;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import com.cringe_studios.cringe_authenticator_library.impl.Base32;
import com.google.protobuf.InvalidProtocolBufferException;
public class OTPParser {
public static OTPData[] parseMigration(Uri uri) throws IllegalArgumentException {
if(!"otpauth-migration".equals(uri.getScheme())) {
throw new IllegalArgumentException("Wrong URI scheme");
}
if(!uri.isHierarchical()) {
throw new IllegalArgumentException("Not a hierarchical URI");
}
String data = uri.getQueryParameter("data");
if(data == null) {
throw new IllegalArgumentException("Missing data");
}
byte[] dataBytes = Base64.decode(data, Base64.DEFAULT);
try {
OTPMigration.MigrationPayload payload = OTPMigration.MigrationPayload.parseFrom(dataBytes);
int count = payload.getOtpParametersCount();
OTPData[] otps = new OTPData[count];
for(int i = 0; i < payload.getOtpParametersCount(); i++) {
OTPMigration.MigrationPayload.OtpParameters params = payload.getOtpParameters(i);
// TODO: issuer
String name = params.getName();
String issuer = params.getIssuer();
OTPType type;
switch(params.getType()) {
case OTP_TYPE_UNSPECIFIED:
case UNRECOGNIZED:
default:
// TODO: be more lenient and only exclude the broken codes
throw new IllegalArgumentException("Unknown OTP type in migration");
case OTP_TYPE_HOTP:
type = OTPType.HOTP;
break;
case OTP_TYPE_TOTP:
type = OTPType.TOTP;
break;
}
String secret = Base32.encode(params.getSecret().toByteArray());
OTPAlgorithm algorithm;
switch(params.getAlgorithm()) {
case ALGORITHM_UNSPECIFIED:
case UNRECOGNIZED:
default:
throw new IllegalArgumentException("Unknown or unsupported algorithm in migration");
case ALGORITHM_SHA1:
algorithm = OTPAlgorithm.SHA1;
break;
case ALGORITHM_SHA256:
algorithm = OTPAlgorithm.SHA256;
break;
case ALGORITHM_SHA512:
algorithm = OTPAlgorithm.SHA512;
break;
case ALGORITHM_MD5:
algorithm = OTPAlgorithm.MD5;
break;
}
int digits;
switch(params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
case UNRECOGNIZED:
default:
throw new IllegalArgumentException("Unknown or unsupported digit count in migration");
case DIGIT_COUNT_SIX:
digits = 6;
break;
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
}
int period = 30; // Google authenticator doesn't support other periods
long counter = params.getCounter();
boolean checksum = false; // Google authenticator doesn't support checksums
otps[i] = new OTPData(name, issuer, type, secret, algorithm, digits, period, counter, checksum);
}
return otps;
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException("Failed to parse migration data", e);
}
}
public static OTPData parse(Uri uri) throws IllegalArgumentException {
if(!"otpauth".equals(uri.getScheme())) {
throw new IllegalArgumentException("Wrong URI scheme");
}
if(!uri.isHierarchical()) {
throw new IllegalArgumentException("Not a hierarchical URI");
}
String type = uri.getHost();
String accountName = uri.getPath();
String issuer = uri.getQueryParameter("issuer");
String secret = uri.getQueryParameter("secret");
String algorithm = uri.getQueryParameter("algorithm");
String digits = uri.getQueryParameter("digits");
@ -58,7 +157,7 @@ public class OTPParser {
}
}
OTPData data = new OTPData(accountName, fType, secret, fAlgorithm, fDigits, fPeriod, fCounter, fChecksum);
OTPData data = new OTPData(accountName, issuer, fType, secret, fAlgorithm, fDigits, fPeriod, fCounter, fChecksum);
String errorMessage = data.validate();
if(errorMessage != null) {

View File

@ -0,0 +1,18 @@
package com.cringe_studios.cringe_authenticator.util;
import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.R;
public class ThemeUtil {
public static void loadTheme(AppCompatActivity activity) {
Integer themeID = SettingsUtil.THEMES.get(SettingsUtil.getTheme(activity));
if(themeID != null) {
activity.setTheme(themeID);
}else {
activity.setTheme(R.style.Theme_CringeAuthenticator_Blue_Green);
}
}
}

View File

@ -14,6 +14,15 @@
android:hint="@string/otp_add_name"
android:autofillHints="" />
<EditText
android:id="@+id/input_issuer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:hint="@string/otp_add_issuer"
android:autofillHints="" />
<EditText
android:id="@+id/input_secret"
android:layout_width="match_parent"

View File

@ -14,6 +14,15 @@
android:hint="@string/otp_add_name"
android:autofillHints="" />
<EditText
android:id="@+id/input_issuer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="text"
android:hint="@string/otp_add_issuer"
android:autofillHints="" />
<EditText
android:id="@+id/input_secret"
android:layout_width="match_parent"

View File

@ -20,7 +20,7 @@
<string name="create_totp_title">Wähle den Code-Typ</string>
<string name="action_new_group">Neue Gruppe</string>
<string name="new_group_missing_title">Du musst einen Namen eingeben</string>
<string name="qr_scanner_failed">Scannen des Codes fehlgeschlagen: %s</string>
<string name="qr_scanner_failed">Scannen fehlgeschlagen: %s</string>
<string name="intro_video_failed">Abspielen des Videos fehlgeschlagen</string>
<string name="edit_otp_title">OTP bearbeiten</string>
<string name="group_delete_title">Löschen?</string>
@ -38,6 +38,7 @@
<string name="settings_biometric_lock">Biometrische Authentifizierung aktivieren</string>
<string name="uri_handler_code_added">Code hinzugefügt</string>
<string name="uri_handler_add_code_title">Code hinzufügen</string>
<string name="uri_handler_create_group">Neue Gruppe erstellen</string>
<string name="theme">Theme</string>
<string name="otp_add_counter">Zähler</string>
<string name="otp_add_checksum">Prüfsumme hinzufügen</string>
@ -45,6 +46,10 @@
<string name="otp_add_name">Name</string>
<string name="otp_add_period">Intervall</string>
<string name="otp_add_error">Hinzufügen des OTP-Codes fehlgeschlagen: %s</string>
<string name="otp_add_issuer">Aussteller (optional)</string>
<string name="otp_add_missing_name">Name fehlt</string>
<string name="qr_scanner_migration_title">OTP-Migration</string>
<string name="qr_scanner_migration_message">Du scheinst zu versuchen, Codes aus einer anderen App zu importieren. Willst du alle Codes in diese Gruppe importieren?</string>
<string-array name="view_edit_delete">
<item>Anzeigen</item>
<item>Bearbeiten</item>

View File

@ -60,7 +60,7 @@
<string name="create_totp_title">Select Code Type</string>
<string name="action_new_group">New Group</string>
<string name="new_group_missing_title">You need to input a name</string>
<string name="qr_scanner_failed">Failed to scan code: %s</string>
<string name="qr_scanner_failed">Scan failed: %s</string>
<string name="intro_video_failed">Failed to play video</string>
<string name="edit_otp_title">Edit OTP</string>
<string name="group_delete_title">Delete?</string>
@ -78,6 +78,7 @@
<string name="settings_biometric_lock">Require biometric unlock</string>
<string name="uri_handler_code_added">Code added</string>
<string name="uri_handler_add_code_title">Add Code</string>
<string name="uri_handler_create_group">Create New Group</string>
<string name="theme">Theme</string>
<string name="otp_add_counter">Counter</string>
<string name="otp_add_checksum">Add Checksum</string>
@ -85,6 +86,10 @@
<string name="otp_add_name">Name</string>
<string name="otp_add_period">Period</string>
<string name="otp_add_error">Failed to update OTP: %s</string>
<string name="otp_add_issuer">Issuer (optional)</string>
<string name="otp_add_missing_name">Missing name</string>
<string name="qr_scanner_migration_title">OTP Migration</string>
<string name="qr_scanner_migration_message">It seems like you\'re trying to import OTP codes from another app. Do you want to import all codes into this group?</string>
<string-array name="view_edit_delete">
<item>View</item>
<item>Edit</item>

View File

@ -2,4 +2,5 @@
plugins {
id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false
id 'com.google.protobuf' version '0.9.3' apply false
}