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 { plugins {
id 'com.android.application' id 'com.android.application'
id 'com.google.protobuf'
} }
android { android {
@ -29,6 +30,32 @@ android {
buildFeatures { buildFeatures {
viewBinding true 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 { dependencies {
@ -43,7 +70,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "androidx.biometric:biometric:1.1.0" 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.mlkit:barcode-scanning:17.1.0'
implementation 'com.google.code.gson:gson:2.8.9' 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-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${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.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" /> <data android:scheme="otpauth" />
<data android:scheme="otpauth-migration" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </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.MenuFragment;
import com.cringe_studios.cringe_authenticator.fragment.NamedFragment; import com.cringe_studios.cringe_authenticator.fragment.NamedFragment;
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment; 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.scanner.QRScannerContract;
import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.NavigationUtil; import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder; 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 com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Locale; 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 //getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); TODO: enable secure flag
Integer themeID = SettingsUtil.THEMES.get(SettingsUtil.getTheme(this)); ThemeUtil.loadTheme(this);
if(themeID != null) {
setTheme(themeID);
}else {
setTheme(R.style.Theme_CringeAuthenticator_Blue_Green);
}
setLocale(SettingsUtil.getLocale(this)); setLocale(SettingsUtil.getLocale(this));
@ -106,7 +103,7 @@ public class MainActivity extends AppCompatActivity {
Fragment fragment = NavigationUtil.getCurrentFragment(this); Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(fragment instanceof GroupFragment) { if(fragment instanceof GroupFragment) {
GroupFragment frag = (GroupFragment) fragment; 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) { if(frag instanceof MenuFragment) {
((MenuFragment) frag).addGroup(group); ((MenuFragment) frag).addGroup(group);
} }
}); }, null);
} }
@Override @Override

View File

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

View File

@ -1,7 +1,5 @@
package com.cringe_studios.cringe_authenticator.model; 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.OTP;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm; import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPException; import com.cringe_studios.cringe_authenticator_library.OTPException;
@ -13,6 +11,7 @@ import java.util.Objects;
public class OTPData implements Serializable { public class OTPData implements Serializable {
private String name; private String name;
private String issuer;
private OTPType type; private OTPType type;
private String secret; private String secret;
private OTPAlgorithm algorithm; private OTPAlgorithm algorithm;
@ -24,8 +23,9 @@ public class OTPData implements Serializable {
// Cached // Cached
private transient OTP otp; 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.name = name;
this.issuer = issuer;
this.type = type; this.type = type;
this.secret = secret; this.secret = secret;
this.algorithm = algorithm; this.algorithm = algorithm;
@ -39,6 +39,10 @@ public class OTPData implements Serializable {
return name; return name;
} }
public String getIssuer() {
return issuer;
}
public OTPType getType() { public OTPType getType() {
return type; 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
OTPData otpData = (OTPData) o; 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 @Override
public int hashCode() { 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.Manifest;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.Image; import android.media.Image;
@ -27,9 +28,12 @@ import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.content.ContextCompat; 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.databinding.ActivityQrScannerBinding;
import com.cringe_studios.cringe_authenticator.model.OTPData; import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.util.OTPParser; 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.common.util.concurrent.ListenableFuture;
import com.google.mlkit.vision.barcode.BarcodeScanner; import com.google.mlkit.vision.barcode.BarcodeScanner;
import com.google.mlkit.vision.barcode.BarcodeScanning; import com.google.mlkit.vision.barcode.BarcodeScanning;
@ -48,12 +52,16 @@ public class QRScannerActivity extends AppCompatActivity {
private BarcodeScanner scanner; private BarcodeScanner scanner;
private boolean process = true;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] {Manifest.permission.CAMERA}, 1234); requestPermissions(new String[] {Manifest.permission.CAMERA}, 1234);
} }
@ -113,6 +121,8 @@ public class QRScannerActivity extends AppCompatActivity {
@OptIn(markerClass = ExperimentalGetImage.class) @OptIn(markerClass = ExperimentalGetImage.class)
@Override @Override
public void analyze(@NonNull ImageProxy image) { public void analyze(@NonNull ImageProxy image) {
if(!process) return;
Image mediaImage = image.getImage(); Image mediaImage = image.getImage();
if(mediaImage != null) { if(mediaImage != null) {
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees()); InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
@ -132,18 +142,40 @@ public class QRScannerActivity extends AppCompatActivity {
if(code == null) return; if(code == null) return;
Uri uri = Uri.parse(code.getRawValue()); Uri uri = Uri.parse(code.getRawValue());
try { process = false;
success(OTPParser.parse(uri)); importUri(uri);
}catch(IllegalArgumentException e) {
error(e.getMessage());
}
} }
}).addOnFailureListener(e -> {}); }).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(); Intent result = new Intent();
result.putExtra("data", data); result.putExtra("data", data);
setResult(Activity.RESULT_OK, result); setResult(Activity.RESULT_OK, result);
@ -157,4 +189,9 @@ public class QRScannerActivity extends AppCompatActivity {
finish(); finish();
} }
private void cancel() {
setResult(RESULT_CANCELED);
finish();
}
} }

View File

@ -28,7 +28,7 @@ public class QRScannerContract extends ActivityResultContract<Void, ScannerResul
return null; 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 { public class ScannerResult {
private OTPData data; private OTPData[] data;
private String errorMessage; private String errorMessage;
public ScannerResult(@NonNull OTPData data) { public ScannerResult(@NonNull OTPData[] data) {
this.data = data; this.data = data;
} }
@ -22,7 +22,7 @@ public class ScannerResult {
return data != null; return data != null;
} }
public OTPData getData() { public OTPData[] getData() {
return data; return data;
} }

View File

@ -1,8 +1,8 @@
package com.cringe_studios.cringe_authenticator.urihandler; package com.cringe_studios.cringe_authenticator.urihandler;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; 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.R;
import com.cringe_studios.cringe_authenticator.model.OTPData; 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.OTPParser;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder; import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
import java.util.List;
public class URIHandlerActivity extends AppCompatActivity { public class URIHandlerActivity extends AppCompatActivity {
@ -22,32 +22,29 @@ public class URIHandlerActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ThemeUtil.loadTheme(this);
Intent intent = getIntent(); Intent intent = getIntent();
if(intent == null) { if(intent == null) {
finish(); finish();
return; 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 { try {
OTPData data = OTPParser.parse(intent.getData()); OTPData data = OTPParser.parse(uri);
List<String> groups = SettingsUtil.getGroups(this); importCodes(data);
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();
}catch(IllegalArgumentException e) { }catch(IllegalArgumentException e) {
AlertDialog dialog = new StyledDialogBuilder(this) AlertDialog dialog = new StyledDialogBuilder(this)
.setTitle(R.string.uri_handler_failed_title) .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.view.View;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; 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 com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class DialogUtil { public class DialogUtil {
@ -81,13 +84,23 @@ public class DialogUtil {
showCodeDialog(context, binding.getRoot(), () -> { showCodeDialog(context, binding.getRoot(), () -> {
try { try {
String name = binding.inputName.getText().toString(); 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(); String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem(); OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem(); int digits = (int) binding.inputDigits.getSelectedItem();
int period = Integer.parseInt(binding.inputPeriod.getText().toString()); int period = Integer.parseInt(binding.inputPeriod.getText().toString());
boolean checksum = binding.inputChecksum.isChecked(); 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(); String errorMessage = data.validate();
if(errorMessage != null) { if(errorMessage != null) {
@ -134,13 +147,23 @@ public class DialogUtil {
showCodeDialog(context, binding.getRoot(), () -> { showCodeDialog(context, binding.getRoot(), () -> {
try { try {
String name = binding.inputName.getText().toString(); 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(); String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem(); OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem(); int digits = (int) binding.inputDigits.getSelectedItem();
int counter = Integer.parseInt(binding.inputCounter.getText().toString()); int counter = Integer.parseInt(binding.inputCounter.getText().toString());
boolean checksum = binding.inputChecksum.isChecked(); 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(); String errorMessage = data.validate();
if(errorMessage != null) { 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(); Context context = inflater.getContext();
DialogCreateGroupBinding binding = DialogCreateGroupBinding.inflate(inflater); DialogCreateGroupBinding binding = DialogCreateGroupBinding.inflate(inflater);
@ -181,7 +204,7 @@ public class DialogUtil {
.setTitle(R.string.action_new_group) .setTitle(R.string.action_new_group)
.setView(binding.getRoot()) .setView(binding.getRoot())
.setPositiveButton(R.string.add, (view, which) -> {}) .setPositiveButton(R.string.add, (view, which) -> {})
.setNegativeButton(R.string.cancel, (view, which) -> {}) .setNegativeButton(R.string.cancel, (view, which) -> { if(onDismiss != null) onDismiss.run(); })
.create(); .create();
dialog.setOnShowListener(d -> { dialog.setOnShowListener(d -> {
@ -194,9 +217,43 @@ public class DialogUtil {
dialog.dismiss(); dialog.dismiss();
callback.accept(binding.createGroupName.getText().toString()); 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(); dialog.show();
} }

View File

@ -1,20 +1,119 @@
package com.cringe_studios.cringe_authenticator.util; package com.cringe_studios.cringe_authenticator.util;
import android.net.Uri; import android.net.Uri;
import android.util.Base64;
import com.cringe_studios.cringe_authenticator.model.OTPData; 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.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType; 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 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 { public static OTPData parse(Uri uri) throws IllegalArgumentException {
if(!"otpauth".equals(uri.getScheme())) { if(!"otpauth".equals(uri.getScheme())) {
throw new IllegalArgumentException("Wrong URI scheme"); throw new IllegalArgumentException("Wrong URI scheme");
} }
if(!uri.isHierarchical()) {
throw new IllegalArgumentException("Not a hierarchical URI");
}
String type = uri.getHost(); String type = uri.getHost();
String accountName = uri.getPath(); String accountName = uri.getPath();
String issuer = uri.getQueryParameter("issuer");
String secret = uri.getQueryParameter("secret"); String secret = uri.getQueryParameter("secret");
String algorithm = uri.getQueryParameter("algorithm"); String algorithm = uri.getQueryParameter("algorithm");
String digits = uri.getQueryParameter("digits"); 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(); String errorMessage = data.validate();
if(errorMessage != null) { 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:hint="@string/otp_add_name"
android:autofillHints="" /> 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 <EditText
android:id="@+id/input_secret" android:id="@+id/input_secret"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -14,6 +14,15 @@
android:hint="@string/otp_add_name" android:hint="@string/otp_add_name"
android:autofillHints="" /> 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 <EditText
android:id="@+id/input_secret" android:id="@+id/input_secret"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -20,7 +20,7 @@
<string name="create_totp_title">Wähle den Code-Typ</string> <string name="create_totp_title">Wähle den Code-Typ</string>
<string name="action_new_group">Neue Gruppe</string> <string name="action_new_group">Neue Gruppe</string>
<string name="new_group_missing_title">Du musst einen Namen eingeben</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="intro_video_failed">Abspielen des Videos fehlgeschlagen</string>
<string name="edit_otp_title">OTP bearbeiten</string> <string name="edit_otp_title">OTP bearbeiten</string>
<string name="group_delete_title">Löschen?</string> <string name="group_delete_title">Löschen?</string>
@ -38,6 +38,7 @@
<string name="settings_biometric_lock">Biometrische Authentifizierung aktivieren</string> <string name="settings_biometric_lock">Biometrische Authentifizierung aktivieren</string>
<string name="uri_handler_code_added">Code hinzugefügt</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_add_code_title">Code hinzufügen</string>
<string name="uri_handler_create_group">Neue Gruppe erstellen</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="otp_add_counter">Zähler</string> <string name="otp_add_counter">Zähler</string>
<string name="otp_add_checksum">Prüfsumme hinzufügen</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_name">Name</string>
<string name="otp_add_period">Intervall</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_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"> <string-array name="view_edit_delete">
<item>Anzeigen</item> <item>Anzeigen</item>
<item>Bearbeiten</item> <item>Bearbeiten</item>

View File

@ -60,7 +60,7 @@
<string name="create_totp_title">Select Code Type</string> <string name="create_totp_title">Select Code Type</string>
<string name="action_new_group">New Group</string> <string name="action_new_group">New Group</string>
<string name="new_group_missing_title">You need to input a name</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="intro_video_failed">Failed to play video</string>
<string name="edit_otp_title">Edit OTP</string> <string name="edit_otp_title">Edit OTP</string>
<string name="group_delete_title">Delete?</string> <string name="group_delete_title">Delete?</string>
@ -78,6 +78,7 @@
<string name="settings_biometric_lock">Require biometric unlock</string> <string name="settings_biometric_lock">Require biometric unlock</string>
<string name="uri_handler_code_added">Code added</string> <string name="uri_handler_code_added">Code added</string>
<string name="uri_handler_add_code_title">Add Code</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="theme">Theme</string>
<string name="otp_add_counter">Counter</string> <string name="otp_add_counter">Counter</string>
<string name="otp_add_checksum">Add Checksum</string> <string name="otp_add_checksum">Add Checksum</string>
@ -85,6 +86,10 @@
<string name="otp_add_name">Name</string> <string name="otp_add_name">Name</string>
<string name="otp_add_period">Period</string> <string name="otp_add_period">Period</string>
<string name="otp_add_error">Failed to update OTP: %s</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"> <string-array name="view_edit_delete">
<item>View</item> <item>View</item>
<item>Edit</item> <item>Edit</item>

View File

@ -2,4 +2,5 @@
plugins { plugins {
id 'com.android.application' version '8.0.2' apply false id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' 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
} }