Add support for Google Authenticator migration data
This commit is contained in:
parent
6c9d6550a4
commit
a3ed96c6b0
@ -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'
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user