Better error handling, Add URI handler, Add support for checksum

This commit is contained in:
MrLetsplay 2023-06-25 23:09:55 +02:00
parent 4154e0e026
commit eb57876fe3
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
12 changed files with 282 additions and 89 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="R38N50464FV" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-06-25T19:29:01.472337342Z" />
</component>
</project>

View File

@ -36,8 +36,18 @@
android:theme="@style/Theme.CringeAuthenticator.None"> android:theme="@style/Theme.CringeAuthenticator.None">
</activity> </activity>
<activity android:name=".scanner.QRScannerActivity" <activity android:name=".scanner.QRScannerActivity"
android:exported="true"
android:theme="@style/Theme.CringeAuthenticator.None"> android:theme="@style/Theme.CringeAuthenticator.None">
</activity> </activity>
<activity android:name=".urihandler.URIHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

View File

@ -7,6 +7,7 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Toast; import android.widget.Toast;
@ -28,12 +29,14 @@ 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.SettingsFragment; import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract; import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract;
import com.cringe_studios.cringe_authenticator.util.DialogCallback;
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_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 java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.function.Supplier;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
@ -76,15 +79,17 @@ public class MainActivity extends AppCompatActivity {
launchApp(); launchApp();
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> { startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
if(obj == null) { // Got some error TODO: show error message if(obj == null) return; // Cancelled
Toast.makeText(this, "Failed to scan code", Toast.LENGTH_LONG).show();
if(!obj.isSuccess()) {
Toast.makeText(this, "Failed to scan code: " + obj.getErrorMessage(), Toast.LENGTH_LONG).show();
return; return;
} }
Fragment fragment = NavigationUtil.getCurrentFragment(this); Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(fragment instanceof DynamicFragment) { if(fragment instanceof DynamicFragment) {
DynamicFragment frag = (DynamicFragment) fragment; DynamicFragment frag = (DynamicFragment) fragment;
frag.addOTP(obj); frag.addOTP(obj.getData());
} }
Log.i("AMOGUS", "Actually got something bruh" + obj); Log.i("AMOGUS", "Actually got something bruh" + obj);
}); });
@ -180,13 +185,13 @@ public class MainActivity extends AppCompatActivity {
} }
private void showTOTPDialog() { private void showTOTPDialog() {
// TODO: checksum option
DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(getLayoutInflater()); DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(getLayoutInflater());
binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, OTPAlgorithm.values())); binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, OTPAlgorithm.values()));
binding.inputDigits.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new Integer[]{6, 7, 8, 9, 10, 11, 12})); binding.inputDigits.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new Integer[]{6, 7, 8, 9, 10, 11, 12}));
showCodeDialog(binding.getRoot(), () -> { showCodeDialog(binding.getRoot(), () -> {
// TODO: handle input
Fragment fragment = NavigationUtil.getCurrentFragment(this); Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(!(fragment instanceof DynamicFragment)) return; if(!(fragment instanceof DynamicFragment)) return true;
try { try {
String name = binding.inputName.getText().toString(); String name = binding.inputName.getText().toString();
@ -195,31 +200,52 @@ public class MainActivity extends AppCompatActivity {
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());
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, period, 0); // TODO: checksum
if(!data.validate()) { OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, period, 0, false);
// TODO: error
return; String errorMessage = data.validate();
if(errorMessage != null) {
showErrorDialog(errorMessage);
return false;
} }
((DynamicFragment) fragment).addOTP(data); ((DynamicFragment) fragment).addOTP(data);
return true;
}catch(NumberFormatException e) { }catch(NumberFormatException e) {
// TODO: error showErrorDialog("Invalid number entered");
return; return false;
} }
}); });
} }
private void showHOTPDialog() { private void showHOTPDialog() {
DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(getLayoutInflater()); DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(getLayoutInflater());
showCodeDialog(binding.getRoot(), () -> {}); showCodeDialog(binding.getRoot(), () -> true);
} }
private void showCodeDialog(View view, Runnable ok) { private void showCodeDialog(View view, DialogCallback ok) {
new AlertDialog.Builder(this) AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Input Code") .setTitle("Input Code")
.setView(view) .setView(view)
.setPositiveButton("Ok", (btnView, which) -> ok.run()) .setPositiveButton("Ok", (btnView, which) -> {})
.setNegativeButton("Cancel", (btnView, which) -> {}) .setNegativeButton("Cancel", (btnView, which) -> {})
.create();
dialog.setOnShowListener(d -> {
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
okButton.setOnClickListener(v -> {
if(ok.callback()) dialog.dismiss();
});
});
dialog.show();
}
private void showErrorDialog(String errorMessage) {
new AlertDialog.Builder(this)
.setTitle("Failed to add code")
.setMessage(errorMessage)
.setPositiveButton("Ok", (dialog, which) -> {})
.show(); .show();
} }

View File

@ -19,11 +19,12 @@ public class OTPData implements Serializable {
private int digits; private int digits;
private int period; private int period;
private long counter; private long counter;
private boolean checksum;
// 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) { public OTPData(String name, OTPType type, String secret, OTPAlgorithm algorithm, int digits, int period, long counter, boolean checksum) {
this.name = name; this.name = name;
this.type = type; this.type = type;
this.secret = secret; this.secret = secret;
@ -31,6 +32,7 @@ public class OTPData implements Serializable {
this.digits = digits; this.digits = digits;
this.period = period; this.period = period;
this.counter = counter; this.counter = counter;
this.checksum = checksum;
} }
public String getName() { public String getName() {
@ -70,19 +72,19 @@ public class OTPData implements Serializable {
this.counter = getOTP().getCounter(); this.counter = getOTP().getCounter();
} }
public boolean validate() { public String validate() {
try { try {
getOTP(); getOTP();
return true; return null;
}catch(IllegalArgumentException | OTPException e) { }catch(IllegalArgumentException | OTPException e) {
return false; return e.getMessage() != null ? e.getMessage() : e.toString();
} }
} }
private OTP getOTP() { private OTP getOTP() {
// TODO: checksum // TODO: checksum
if(otp != null) return otp; if(otp != null) return otp;
return otp = OTP.createNewOTP(type, secret, algorithm, digits, counter, period, false); return otp = OTP.createNewOTP(type, secret, algorithm, digits, counter, period, checksum);
} }
@NonNull @NonNull
@ -96,6 +98,7 @@ public class OTPData implements Serializable {
", digits=" + digits + ", digits=" + digits +
", period=" + period + ", period=" + period +
", counter=" + counter + ", counter=" + counter +
", checksum=" + checksum +
'}'; '}';
} }

View File

@ -3,11 +3,12 @@ package com.cringe_studios.cringe_authenticator.scanner;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.Image; import android.media.Image;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.view.MotionEvent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -16,19 +17,22 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera; import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector; import androidx.camera.core.CameraSelector;
import androidx.camera.core.ExperimentalGetImage; import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.ImageAnalysis; import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy; import androidx.camera.core.ImageProxy;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.Preview; import androidx.camera.core.Preview;
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.OTPData; import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.databinding.ActivityQrScannerBinding; import com.cringe_studios.cringe_authenticator.databinding.ActivityQrScannerBinding;
import com.cringe_studios.cringe_authenticator.util.OTPParser;
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.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.BarcodeScannerOptions;
import com.google.mlkit.vision.barcode.BarcodeScanning; import com.google.mlkit.vision.barcode.BarcodeScanning;
import com.google.mlkit.vision.barcode.common.Barcode; import com.google.mlkit.vision.barcode.common.Barcode;
import com.google.mlkit.vision.common.InputImage; import com.google.mlkit.vision.common.InputImage;
@ -37,6 +41,8 @@ import java.util.concurrent.ExecutionException;
public class QRScannerActivity extends AppCompatActivity { public class QRScannerActivity extends AppCompatActivity {
public static final int RESULT_ERROR = -2;
private ActivityQrScannerBinding binding; private ActivityQrScannerBinding binding;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture; private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
@ -47,7 +53,7 @@ public class QRScannerActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 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);
} }
@ -56,7 +62,6 @@ public class QRScannerActivity extends AppCompatActivity {
cameraProviderFuture = ProcessCameraProvider.getInstance(this); cameraProviderFuture = ProcessCameraProvider.getInstance(this);
cameraProviderFuture.addListener(() -> { cameraProviderFuture.addListener(() -> {
Log.i("AMOGUS", "Got something");
try { try {
ProcessCameraProvider cameraProvider = cameraProviderFuture.get(); ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
bindPreview(cameraProvider); bindPreview(cameraProvider);
@ -66,13 +71,14 @@ public class QRScannerActivity extends AppCompatActivity {
} }
}, ContextCompat.getMainExecutor(this)); }, ContextCompat.getMainExecutor(this));
BarcodeScannerOptions opts = new BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build();
scanner = BarcodeScanning.getClient(); scanner = BarcodeScanning.getClient();
} }
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
}
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) { void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder() Preview preview = new Preview.Builder()
.build(); .build();
@ -84,15 +90,24 @@ public class QRScannerActivity extends AppCompatActivity {
preview.setSurfaceProvider(binding.preview.getSurfaceProvider()); preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
ImageAnalysis analysis = new ImageAnalysis.Builder() ImageAnalysis analysis = new ImageAnalysis.Builder()
//.setTargetResolution(new Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build(); .build();
analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new Amogus()); analysis.setAnalyzer(ContextCompat.getMainExecutor(this), new QRAnalyzer());
Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, analysis); Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, analysis);
binding.preview.setOnTouchListener((view, event) -> {
if(event.getAction() != MotionEvent.ACTION_DOWN) return true;
view.performClick();
MeteringPointFactory factory = new SurfaceOrientedMeteringPointFactory(binding.preview.getWidth(), binding.preview.getHeight());
camera.getCameraControl().startFocusAndMetering(new FocusMeteringAction.Builder(factory.createPoint(event.getX(), event.getY())).build());
return true;
});
} }
private class Amogus implements ImageAnalysis.Analyzer { private class QRAnalyzer implements ImageAnalysis.Analyzer {
@OptIn(markerClass = ExperimentalGetImage.class) @OptIn(markerClass = ExperimentalGetImage.class)
@Override @Override
@ -101,13 +116,11 @@ public class QRScannerActivity extends AppCompatActivity {
if(mediaImage != null) { if(mediaImage != null) {
InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees()); InputImage input = InputImage.fromMediaImage(mediaImage, image.getImageInfo().getRotationDegrees());
scanner.process(input).addOnSuccessListener(barcodes -> { scanner.process(input).addOnSuccessListener(barcodes -> {
//Log.i("AMOGUS", "found " + barcodes.size() + " codes");
image.close(); image.close();
if(barcodes.size() >= 1) { if(barcodes.size() >= 1) {
Barcode code = null; Barcode code = null;
Log.i("AMOGUS", "TYPE " + barcodes.get(0).getValueType());
for(Barcode c : barcodes) { for(Barcode c : barcodes) {
if(c.getValueType() == Barcode.TYPE_TEXT) { if(c.getValueType() == Barcode.TYPE_TEXT) {
code = c; code = c;
@ -118,58 +131,10 @@ 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());
Log.i("AMOGUS", code.getRawValue());
Log.i("AMOGUS", uri.getHost());
Log.i("AMOGUS", uri.getPath());
String type = uri.getHost();
String accountName = uri.getPath();
String secret = uri.getQueryParameter("secret");
String algorithm = uri.getQueryParameter("algorithm");
String digits = uri.getQueryParameter("digits");
String period = uri.getQueryParameter("period");
String counter = uri.getQueryParameter("counter");
if(type == null || secret == null) {
error("Missing params");
return;
}
OTPType fType;
try { try {
fType = OTPType.valueOf(type.toUpperCase()); success(OTPParser.parse(uri));
}catch(IllegalArgumentException e) { }catch(IllegalArgumentException e) {
Log.i("AMOGUS", e.toString()); error(e.getMessage());
error("Failed to parse OTP parameters");
return;
}
if(fType == OTPType.HOTP && counter == null) {
error("Missing params");
return;
}
if(accountName == null || accountName.length() < 2 /* Because path is /accName, so 2 letters for acc with 1 letter name */) {
// TODO: error
Log.i("AMOGUS", "Missing params");
error("Missing OTP parameters");
return;
}
accountName = accountName.substring(1);
try {
// 0 or null for defaults (handled by Cringe-Authenticator-Library)
OTPAlgorithm fAlgorithm = algorithm == null ? null : OTPAlgorithm.valueOf(algorithm.toUpperCase());
int fDigits = digits == null ? 0 : Integer.parseInt(digits);
int fPeriod = period == null ? 0 : Integer.parseInt(period);
int fCounter = counter == null ? 0 : Integer.parseInt(counter);
success(new OTPData(accountName, fType, secret, fAlgorithm, fDigits, fPeriod, fCounter));
}catch(IllegalArgumentException e) {
Log.i("AMOGUS", e.toString());
error("Failed to parse OTP parameters");
return;
} }
} }
}).addOnFailureListener(e -> {}); }).addOnFailureListener(e -> {});
@ -185,7 +150,9 @@ public class QRScannerActivity extends AppCompatActivity {
} }
private void error(String errorMessage) { private void error(String errorMessage) {
setResult(Activity.RESULT_CANCELED, null); Intent result = new Intent();
result.putExtra("error", errorMessage);
setResult(RESULT_ERROR, result);
finish(); finish();
} }

View File

@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
import com.cringe_studios.cringe_authenticator.OTPData; import com.cringe_studios.cringe_authenticator.OTPData;
public class QRScannerContract extends ActivityResultContract<Void, OTPData> { public class QRScannerContract extends ActivityResultContract<Void, ScannerResult> {
@NonNull @NonNull
@Override @Override
@ -19,12 +19,16 @@ public class QRScannerContract extends ActivityResultContract<Void, OTPData> {
} }
@Override @Override
public @Nullable OTPData parseResult(int result, @Nullable Intent intent) { public @Nullable ScannerResult parseResult(int result, @Nullable Intent intent) {
if(result != Activity.RESULT_OK || intent == null) { if(result != Activity.RESULT_OK || intent == null) {
if(intent != null) {
return new ScannerResult(intent.getStringExtra("error"));
}
return null; return null;
} }
return (OTPData) intent.getSerializableExtra("data"); return new ScannerResult((OTPData) intent.getSerializableExtra("data"));
} }
} }

View File

@ -0,0 +1,32 @@
package com.cringe_studios.cringe_authenticator.scanner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.cringe_studios.cringe_authenticator.OTPData;
public class ScannerResult {
private OTPData data;
private String errorMessage;
public ScannerResult(@NonNull OTPData data) {
this.data = data;
}
public ScannerResult(@Nullable String errorMessage) {
this.errorMessage = errorMessage;
}
public boolean isSuccess() {
return data != null;
}
public OTPData getData() {
return data;
}
public String getErrorMessage() {
return errorMessage;
}
}

View File

@ -0,0 +1,44 @@
package com.cringe_studios.cringe_authenticator.urihandler;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.util.OTPParser;
public class URIHandlerActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if(intent == null) {
finish();
return;
}
try {
OTPData data = OTPParser.parse(intent.getData());
// TODO: choose group, add code
Toast.makeText(this, "Code received", Toast.LENGTH_LONG).show();
finish();
}catch(IllegalArgumentException e) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Failed to add code")
.setMessage(e.getMessage())
.setPositiveButton("Ok", (d, which) -> finish())
.create();
dialog.setOnDismissListener(d -> finish());
dialog.show();
}
}
}

View File

@ -0,0 +1,7 @@
package com.cringe_studios.cringe_authenticator.util;
public interface DialogCallback {
public boolean callback();
}

View File

@ -0,0 +1,74 @@
package com.cringe_studios.cringe_authenticator.util;
import android.net.Uri;
import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType;
public class OTPParser {
public static OTPData parse(Uri uri) throws IllegalArgumentException {
if(!"otpauth".equals(uri.getScheme())) {
throw new IllegalArgumentException("Wrong URI scheme");
}
String type = uri.getHost();
String accountName = uri.getPath();
String secret = uri.getQueryParameter("secret");
String algorithm = uri.getQueryParameter("algorithm");
String digits = uri.getQueryParameter("digits");
String period = uri.getQueryParameter("period");
String counter = uri.getQueryParameter("counter");
String checksum = uri.getQueryParameter("checksum");
if(type == null || secret == null) {
throw new IllegalArgumentException("Missing params");
}
OTPType fType;
try {
fType = OTPType.valueOf(type.toUpperCase());
}catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Failed to parse OTP parameters");
}
if(fType == OTPType.HOTP && counter == null) {
throw new IllegalArgumentException("Missing required parameters");
}
if(accountName == null || accountName.length() < 2 /* Because path is /accName, so 2 letters for acc with 1 letter name */) {
throw new IllegalArgumentException("Missing required parameters");
}
accountName = accountName.substring(1);
try {
// 0 or null for defaults (handled by Cringe-Authenticator-Library)
OTPAlgorithm fAlgorithm = algorithm == null ? null : OTPAlgorithm.valueOf(algorithm.toUpperCase());
int fDigits = digits == null ? 0 : Integer.parseInt(digits);
int fPeriod = period == null ? 0 : Integer.parseInt(period);
int fCounter = counter == null ? 0 : Integer.parseInt(counter);
boolean fChecksum = false;
if(checksum != null) {
switch(checksum) {
case "true": fChecksum = true; break;
case "false": break;
default: throw new IllegalArgumentException("Checksum must be set to 'true' or 'false'");
}
}
OTPData data = new OTPData(accountName, fType, secret, fAlgorithm, fDigits, fPeriod, fCounter, fChecksum);
String errorMessage = data.validate();
if(errorMessage != null) {
throw new IllegalArgumentException(errorMessage);
}
return data;
}catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Failed to parse OTP parameters", e);
}
}
}

View File

@ -41,6 +41,7 @@ public class SettingsUtil {
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
// TODO: refactor
public static List<OTPData> getOTPs(SharedPreferences prefs, String group) { public static List<OTPData> getOTPs(SharedPreferences prefs, String group) {
String currentOTPs = prefs.getString("group." + group, "[]"); String currentOTPs = prefs.getString("group." + group, "[]");
return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class)); return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class));
@ -73,4 +74,12 @@ public class SettingsUtil {
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("theme", THEME_NAMES.get(0)); return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getString("theme", THEME_NAMES.get(0));
} }
public static void enableSuperSecretHamburgers(Context ctx) {
ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).edit().putBoolean("iLikeHamburgers", true).apply();
}
public static boolean isSuperSecretHamburgersEnabled(Context ctx) {
return ctx.getSharedPreferences(GENERAL_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("iLikeHamburgers", false);
}
} }

View File

@ -1,9 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Base.Theme.CringeAuthenticator" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Base.Theme.CringeAuthenticator" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">?attr/colorTheme1</item>
<item name="colorPrimaryDark">@android:color/transparent</item> <item name="colorPrimaryDark">@android:color/transparent</item>
<item name="colorSecondary">@color/colorSecondary</item> <item name="colorSecondary">?attr/colorTheme2</item>
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="colorTheme1">#FF00FF</item> <item name="colorTheme1">#FF00FF</item>