Crypto (WIP)

This commit is contained in:
MrLetsplay 2023-09-17 19:19:23 +02:00
parent 0025f37e99
commit e80d63336d
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
13 changed files with 564 additions and 15 deletions

View File

@ -10,8 +10,8 @@
<option name="gradleJvm" value="jbr-17" /> <option name="gradleJvm" value="jbr-17" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator" />
<option value="$PROJECT_DIR$/app" /> <option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator/app" />
</set> </set>
</option> </option>
</GradleProjectSettings> </GradleProjectSettings>

View File

@ -5,6 +5,8 @@ import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTI
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.security.keystore.KeyProtection;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -22,6 +24,8 @@ import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding; import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding; import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding;
import com.cringe_studios.cringe_authenticator.fragment.AboutFragment; import com.cringe_studios.cringe_authenticator.fragment.AboutFragment;
@ -34,14 +38,28 @@ 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.OTPDatabase;
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.util.ThemeUtil;
import com.cringe_studios.cringe_authenticator_library.OTPType; import com.cringe_studios.cringe_authenticator_library.OTPType;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import javax.crypto.SecretKey;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final long LOCK_TIMEOUT = 10000; private static final long LOCK_TIMEOUT = 10000;
@ -58,13 +76,30 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
/*try {
byte[] salt = Crypto.generateSalt();
SecretKey key = Crypto.generateKey("HELLO", salt);
Log.i("UWUSECRET", key.toString());
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
ks.setEntry("", new KeyStore.SecretKeyEntry(key), null);
} catch (CryptoException | KeyStoreException | CertificateException | IOException |
NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}*/
//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
ThemeUtil.loadTheme(this); ThemeUtil.loadTheme(this);
setLocale(SettingsUtil.getLocale(this)); setLocale(SettingsUtil.getLocale(this));
Executor executor = ContextCompat.getMainExecutor(this); OTPDatabase.promptLoadDatabase(this, () -> {
launchApp();
}, () -> finishAffinity());
/*Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt prompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { BiometricPrompt prompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() {
@Override @Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
@ -90,7 +125,7 @@ public class MainActivity extends AppCompatActivity {
prompt.authenticate(info); prompt.authenticate(info);
}else { }else {
launchApp(); launchApp();
} }*/
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> { startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
if(obj == null) return; // Cancelled if(obj == null) return; // Cancelled
@ -252,4 +287,5 @@ public class MainActivity extends AppCompatActivity {
outState.putLong("pauseTime", pauseTime); outState.putLong("pauseTime", pauseTime);
} }
} }
} }

View File

@ -0,0 +1,84 @@
package com.cringe_studios.cringe_authenticator.crypto;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Crypto {
public static byte[] generateHash(CryptoParameters parameters, String password) throws CryptoException {
Argon2Parameters params = new Argon2Parameters.Builder()
.withVersion(parameters.getArgon2Version())
.withIterations(parameters.getArgon2Iterations())
.withMemoryAsKB(parameters.getArgon2Memory())
.withParallelism(parameters.getArgon2Parallelism())
.withSalt(parameters.getSalt())
.build();
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
byte[] result = new byte[parameters.getEncryptionAESKeyLength()];
generator.generateBytes(password.getBytes(StandardCharsets.UTF_8), result);
return result;
}
public static SecretKey generateKey(CryptoParameters parameters, String password) throws CryptoException {
byte[] hash = generateHash(parameters, password);
return new SecretKeySpec(hash, "AES");
}
private static byte[] generateBytes(int length) {
SecureRandom r = new SecureRandom();
byte[] bytes = new byte[length];
r.nextBytes(bytes);
return bytes;
}
public static byte[] generateSalt(CryptoParameters parameters) {
return generateBytes(parameters.getEncryptionSaltLength());
}
public static byte[] generateIV(CryptoParameters parameters) {
return generateBytes(parameters.getEncryptionIVLength());
}
public static byte[] encrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, parameters.getIV());
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
return cipher.doFinal(bytes);
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
IllegalBlockSizeException e) {
throw new CryptoException(e);
}
}
public static byte[] decrypt(CryptoParameters parameters, byte[] bytes, SecretKey key) throws CryptoException {
try {
Cipher cipher = Cipher.getInstance(parameters.getEncryptionAlgorithm());
GCMParameterSpec spec = new GCMParameterSpec(parameters.getEncryptionGCMTagLength() * 8, parameters.getIV());
cipher.init(Cipher.DECRYPT_MODE, key, spec);
return cipher.doFinal(bytes);
}catch(NoSuchAlgorithmException | NoSuchPaddingException |
InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException |
IllegalBlockSizeException e) {
throw new CryptoException(e);
}
}
}

View File

@ -0,0 +1,18 @@
package com.cringe_studios.cringe_authenticator.crypto;
public class CryptoException extends Exception {
public CryptoException() {
}
public CryptoException(String message) {
super(message);
}
public CryptoException(String message, Throwable cause) {
super(message, cause);
}
public CryptoException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,86 @@
package com.cringe_studios.cringe_authenticator.crypto;
import org.bouncycastle.crypto.params.Argon2Parameters;
public class CryptoParameters {
private final String hashType = "argon2";
private final int argon2Version = Argon2Parameters.ARGON2_VERSION_13;
private final int argon2Iterations = 2;
private final int argon2Memory = 16384;
private final int argon2Parallelism = 1;
private final String encryptionAlgorithm = "AES/GCM/NoPadding";
private final int encryptionGCMTagLength = 16;
private final int encryptionIVLength = 12;
private final int encryptionAESKeyLength = 32;
private final int encryptionSaltLength = 16;
private byte[] salt;
private byte[] iv;
private CryptoParameters() {}
private void init(byte[] salt, byte[] iv) {
this.salt = salt;
this.iv = iv;
}
public byte[] getSalt() {
return salt;
}
public byte[] getIV() {
return iv;
}
public String getHashType() {
return hashType;
}
public int getArgon2Version() {
return argon2Version;
}
public int getArgon2Iterations() {
return argon2Iterations;
}
public int getArgon2Memory() {
return argon2Memory;
}
public int getArgon2Parallelism() {
return argon2Parallelism;
}
public String getEncryptionAlgorithm() {
return encryptionAlgorithm;
}
public int getEncryptionGCMTagLength() {
return encryptionGCMTagLength;
}
public int getEncryptionIVLength() {
return encryptionIVLength;
}
public int getEncryptionAESKeyLength() {
return encryptionAESKeyLength;
}
public int getEncryptionSaltLength() {
return encryptionSaltLength;
}
public static CryptoParameters createNew() {
CryptoParameters params = new CryptoParameters();
byte[] salt = Crypto.generateSalt(params);
byte[] iv = Crypto.generateIV(params);
params.init(salt, iv);
return params;
}
}

View File

@ -11,12 +11,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.cringe_studios.cringe_authenticator.R; import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.FragmentGroupBinding; import com.cringe_studios.cringe_authenticator.databinding.FragmentGroupBinding;
import com.cringe_studios.cringe_authenticator.model.OTPData; import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter; import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter;
import com.cringe_studios.cringe_authenticator.otplist.OTPListItem; import com.cringe_studios.cringe_authenticator.otplist.OTPListItem;
import com.cringe_studios.cringe_authenticator.util.DialogUtil; import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.FabUtil; import com.cringe_studios.cringe_authenticator.util.FabUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
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_library.OTPException; import com.cringe_studios.cringe_authenticator_library.OTPException;
@ -90,7 +93,13 @@ public class GroupFragment extends NamedFragment {
break; break;
case 2: case 2:
DialogUtil.showChooseGroupDialog(requireContext(), group -> { DialogUtil.showChooseGroupDialog(requireContext(), group -> {
SettingsUtil.addOTP(requireContext(), group, data); if(OTPDatabase.getLoadedDatabase() == null) {
// TODO: prompt user
return;
}
OTPDatabase.getLoadedDatabase().addOTP(group, data);
// TODO: save
otpListAdapter.remove(data); otpListAdapter.remove(data);
saveOTPs(); saveOTPs();
}, null); }, null);
@ -113,20 +122,32 @@ public class GroupFragment extends NamedFragment {
} }
private void saveOTPs() { private void saveOTPs() {
SettingsUtil.updateOTPs(requireContext(), groupID, otpListAdapter.getItems()); //SettingsUtil.updateOTPs(requireContext(), groupID, otpListAdapter.getItems());
if(OTPDatabase.getLoadedDatabase() == null) {
// TODO: prompt user
return;
}
OTPDatabase.getLoadedDatabase().updateOTPs(groupID, otpListAdapter.getItems());
try {
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
refreshCodes(); refreshCodes();
} }
private void loadOTPs() { private void loadOTPs() {
List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupID); /*List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupID); TODO
for(OTPData otp : data) { for(OTPData otp : data) {
otpListAdapter.add(otp); otpListAdapter.add(otp);
} }*/
} }
public void addOTP(OTPData data) { public void addOTP(OTPData data) {
SettingsUtil.addOTP(requireContext(), groupID, data); //SettingsUtil.addOTP(requireContext(), groupID, data); TODO
otpListAdapter.add(data); otpListAdapter.add(data);
} }

View File

@ -4,6 +4,7 @@ import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRON
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -15,13 +16,21 @@ import androidx.annotation.Nullable;
import androidx.biometric.BiometricManager; import androidx.biometric.BiometricManager;
import com.cringe_studios.cringe_authenticator.MainActivity; import com.cringe_studios.cringe_authenticator.MainActivity;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding; import com.cringe_studios.cringe_authenticator.databinding.FragmentSettingsBinding;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.FabUtil; import com.cringe_studios.cringe_authenticator.util.FabUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import javax.crypto.SecretKey;
public class SettingsFragment extends NamedFragment { public class SettingsFragment extends NamedFragment {
private FragmentSettingsBinding binding; private FragmentSettingsBinding binding;
@ -62,6 +71,37 @@ public class SettingsFragment extends NamedFragment {
} }
}); });
binding.settingsEnableEncryption.setOnCheckedChangeListener((view, checked) -> {
if(!OTPDatabase.isDatabaseLoaded()) {
// TODO: prompt user
}
if(checked) {
DialogUtil.showInputPasswordDialog(requireContext(), password -> {
CryptoParameters params = CryptoParameters.createNew();
Log.d("Crypto", "Created new crypto params");
try {
SecretKey key = Crypto.generateKey(params, password);
Log.d("Crypto", "Generated key");
OTPDatabase.encrypt(requireContext(), key, params);
SettingsUtil.enableEncryption(requireContext(), params);
Log.d("Crypto", "DB encryption enabled");
} catch (CryptoException | OTPDatabaseException e) {
throw new RuntimeException(e); // TODO
}
}, null);
}else {
try {
OTPDatabase.decrypt(requireContext());
SettingsUtil.disableEncryption(requireContext());
Log.d("Crypto", "DB encryption disabled");
} catch (OTPDatabaseException | CryptoException e) {
throw new RuntimeException(e); // TODO
}
}
});
binding.settingsEnableIntroVideo.setChecked(SettingsUtil.isIntroVideoEnabled(requireContext())); binding.settingsEnableIntroVideo.setChecked(SettingsUtil.isIntroVideoEnabled(requireContext()));
binding.settingsEnableIntroVideo.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setEnableIntroVideo(requireContext(), checked)); binding.settingsEnableIntroVideo.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setEnableIntroVideo(requireContext(), checked));

View File

@ -10,8 +10,11 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; 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.crypto.CryptoException;
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.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
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;
@ -61,7 +64,18 @@ public class URIHandlerActivity extends AppCompatActivity {
private void importCodes(OTPData... data) { private void importCodes(OTPData... data) {
DialogUtil.showChooseGroupDialog(this, group -> { DialogUtil.showChooseGroupDialog(this, group -> {
for(OTPData d : data) SettingsUtil.addOTP(this, group, d); for(OTPData d : data) {
if(OTPDatabase.getLoadedDatabase() == null) {
// TODO: prompt user
}
OTPDatabase.getLoadedDatabase().addOTP(group, d);
try {
OTPDatabase.saveDatabase(this, SettingsUtil.getCryptoParameters(this));
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(this, e.toString());
}
}
Toast.makeText(this, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show(); Toast.makeText(this, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show();
}, this::finishAndRemoveTask); }, this::finishAndRemoveTask);
} }

View File

@ -1,10 +1,12 @@
package com.cringe_studios.cringe_authenticator.util; package com.cringe_studios.cringe_authenticator.util;
import android.content.Context; import android.content.Context;
import android.text.InputType;
import android.view.LayoutInflater; 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.EditText;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
@ -260,4 +262,25 @@ public class DialogUtil {
dialog.show(); dialog.show();
} }
public static void showInputPasswordDialog(Context context, Consumer<String> callback, Runnable onDismiss) {
EditText passwordField = new EditText(context);
passwordField.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
AlertDialog dialog = new StyledDialogBuilder(context)
.setTitle("Input Password")
.setView(passwordField) // TODO: better layout
.setPositiveButton("Ok", (d, which) -> {
if(passwordField.getText().length() == 0) {
DialogUtil.showErrorDialog(context, "You need to enter a password");
return;
}
callback.accept(passwordField.getText().toString());
})
.setNegativeButton(R.string.cancel, (d, which) -> { if(onDismiss != null) onDismiss.run(); })
.setOnCancelListener(d -> { if(onDismiss != null) onDismiss.run(); })
.create();
dialog.show();
}
} }

View File

@ -0,0 +1,188 @@
package com.cringe_studios.cringe_authenticator.util;
import android.content.Context;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.SecretKey;
public class OTPDatabase {
public static final String DB_FILE_NAME = "db";
private static OTPDatabase loadedDatabase;
private static SecretKey loadedKey;
private final Map<String, List<OTPData>> otps;
private OTPDatabase(Map<String, List<OTPData>> otps) {
this.otps = otps;
}
public List<OTPData> getOTPs(String groupId) {
List<OTPData> o = otps.get(groupId);
if(o == null) return Collections.emptyList();
return o;
}
public void addOTP(String groupId, OTPData o) {
// TODO: check for code with same name
List<OTPData> os = new ArrayList<>(getOTPs(groupId));
os.add(o);
updateOTPs(groupId, os);
}
public void updateOTPs(String groupId, List<OTPData> o) {
otps.put(groupId, o);
}
public void removeOTPs(String groupId) {
otps.remove(groupId);
}
public static void promptLoadDatabase(Context ctx, Runnable success, Runnable failure) {
if(isDatabaseLoaded()) {
success.run();
return;
}
if(!SettingsUtil.isDatabaseEncrypted(ctx)) {
try {
loadDatabase(ctx, null);
if(success != null) success.run();
} catch (OTPDatabaseException | CryptoException e) {
throw new RuntimeException(e); // TODO
}
return;
}
DialogUtil.showErrorDialog(ctx, "LOAD DB!"); // TODO: implement
DialogUtil.showInputPasswordDialog(ctx, password -> {
try {
SecretKey key = Crypto.generateKey(SettingsUtil.getCryptoParameters(ctx), password);
loadDatabase(ctx, key);
if(success != null) success.run();
} catch (CryptoException | OTPDatabaseException e) {
failure.run(); // TODO: show error
}
}, failure);
}
public static OTPDatabase loadDatabase(Context context, SecretKey key) throws OTPDatabaseException, CryptoException {
File file = new File(context.getFilesDir(), DB_FILE_NAME);
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
throw new OTPDatabaseException(e);
}
}
try(FileInputStream fIn = new FileInputStream(file)) {
ByteBuffer fileBuffer = ByteBuffer.allocate((int) file.length());
byte[] buffer = new byte[1024];
int len;
while((len = fIn.read(buffer)) > 0) {
fileBuffer.put(buffer, 0, len);
}
if(key == null) {
loadedDatabase = loadFromBytes(fileBuffer.array());
loadedKey = null;
return loadedDatabase;
}
loadedDatabase = loadFromBytes(Crypto.decrypt(SettingsUtil.getCryptoParameters(context), fileBuffer.array(), key));
loadedKey = key;
return loadedDatabase;
}catch(IOException e) {
throw new OTPDatabaseException(e);
}
}
private static OTPDatabase loadFromBytes(byte[] bytes) throws OTPDatabaseException {
try {
Map<String, List<OTPData>> data = new HashMap<>();
if(bytes.length == 0) return new OTPDatabase(data);
JsonObject object = SettingsUtil.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), JsonObject.class);
for(String key : object.keySet()) {
data.put(key, new ArrayList<>(Arrays.asList(SettingsUtil.GSON.fromJson(object.getAsJsonArray(key), OTPData[].class))));
}
return new OTPDatabase(data);
}catch(JsonSyntaxException e) {
throw new OTPDatabaseException("Password incorrect or database is corrupted");
}catch(ClassCastException e) {
throw new OTPDatabaseException("Invalid database");
}
}
private static byte[] convertToBytes(OTPDatabase db) {
JsonObject object = new JsonObject();
for(Map.Entry<String, List<OTPData>> en : db.otps.entrySet()) {
object.add(en.getKey(), SettingsUtil.GSON.toJsonTree(en.getValue().toArray(new OTPData[0])));
}
return object.toString().getBytes(StandardCharsets.UTF_8);
}
public static void saveDatabase(Context ctx, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
File file = new File(ctx.getFilesDir(), DB_FILE_NAME);
byte[] dbBytes = convertToBytes(loadedDatabase);
if(loadedKey != null) {
dbBytes = Crypto.encrypt(parameters, dbBytes, loadedKey);
}
try(FileOutputStream fOut = new FileOutputStream(file)) {
fOut.write(dbBytes);
} catch (IOException e) {
throw new OTPDatabaseException(e);
}
}
public static void unloadDatabase() {
loadedDatabase = null;
}
public static OTPDatabase getLoadedDatabase() {
return loadedDatabase;
}
public static void encrypt(Context ctx, SecretKey key, CryptoParameters parameters) throws OTPDatabaseException, CryptoException {
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
loadedKey = key;
saveDatabase(ctx, parameters);
}
public static void decrypt(Context ctx) throws OTPDatabaseException, CryptoException {
if(!isDatabaseLoaded()) throw new IllegalStateException("Database is not loaded");
loadedKey = null;
saveDatabase(ctx, null);
}
public static boolean isDatabaseLoaded() {
return loadedDatabase != null;
}
}

View File

@ -0,0 +1,18 @@
package com.cringe_studios.cringe_authenticator.util;
public class OTPDatabaseException extends Exception {
public OTPDatabaseException() {
}
public OTPDatabaseException(String message) {
super(message);
}
public OTPDatabaseException(String message, Throwable cause) {
super(message, cause);
}
public OTPDatabaseException(Throwable cause) {
super(cause);
}
}

View File

@ -3,10 +3,8 @@ package com.cringe_studios.cringe_authenticator.util;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import androidx.annotation.NonNull;
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.crypto.CryptoParameters;
import com.google.gson.Gson; import com.google.gson.Gson;
import java.util.ArrayList; import java.util.ArrayList;
@ -38,7 +36,7 @@ public class SettingsUtil {
GROUPS_PREFS_NAME = "groups", GROUPS_PREFS_NAME = "groups",
GENERAL_PREFS_NAME = "general"; GENERAL_PREFS_NAME = "general";
private static final Gson GSON = new Gson(); public static final Gson GSON = new Gson();
public static List<String> getGroups(Context ctx) { public static List<String> getGroups(Context ctx) {
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE); SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
@ -65,7 +63,7 @@ public class SettingsUtil {
deleteGroupData(ctx, group); deleteGroupData(ctx, group);
} }
public static List<OTPData> getOTPs(Context ctx, String group) { /*public static List<OTPData> getOTPs(Context ctx, String group) {
String currentOTPs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".otps", "[]"); String currentOTPs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("group." + group + ".otps", "[]");
return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class)); return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class));
} }
@ -85,6 +83,28 @@ public class SettingsUtil {
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit() ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0]))) .putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0])))
.apply(); .apply();
}*/
public static boolean isDatabaseEncrypted(Context ctx) {
return ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getBoolean("encryption", false);
}
public static void enableEncryption(Context ctx, CryptoParameters parameters) {
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putBoolean("encryption", true)
.putString("encryption.parameters", GSON.toJson(parameters))
.apply();
}
public static void disableEncryption(Context ctx) {
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putBoolean("encryption", false)
.remove("encryption.parameters")
.apply();
}
public static CryptoParameters getCryptoParameters(Context ctx) {
return GSON.fromJson(ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).getString("encryption.parameters", "{}"), CryptoParameters.class);
} }
public static String getGroupName(Context ctx, String group) { public static String getGroupName(Context ctx, String group) {

View File

@ -51,6 +51,7 @@
<string name="qr_scanner_migration_part">Code %d of %d scanned</string> <string name="qr_scanner_migration_part">Code %d of %d scanned</string>
<string name="screen_security">Screen Security</string> <string name="screen_security">Screen Security</string>
<string name="hide_codes">Hide Codes</string> <string name="hide_codes">Hide Codes</string>
<string name="enable_encryption">Enable encryption</string>
<string-array name="view_edit_move_delete"> <string-array name="view_edit_move_delete">
<item>View</item> <item>View</item>
<item>Edit</item> <item>Edit</item>