Crypto (WIP)
This commit is contained in:
parent
0025f37e99
commit
e80d63336d
@ -10,8 +10,8 @@
|
||||
<option name="gradleJvm" value="jbr-17" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator" />
|
||||
<option value="/mnt/sshd/Files/Desktop/testing/android/Cringe-Authenticator/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
@ -5,6 +5,8 @@ import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTI
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.security.keystore.KeyProtection;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@ -22,6 +24,8 @@ import androidx.biometric.BiometricPrompt;
|
||||
import androidx.core.content.ContextCompat;
|
||||
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.DialogInputCodeChoiceBinding;
|
||||
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.util.DialogUtil;
|
||||
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.StyledDialogBuilder;
|
||||
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
|
||||
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.concurrent.Executor;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final long LOCK_TIMEOUT = 10000;
|
||||
@ -58,13 +76,30 @@ public class MainActivity extends AppCompatActivity {
|
||||
protected void onCreate(Bundle 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
|
||||
|
||||
ThemeUtil.loadTheme(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() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
@ -90,7 +125,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
prompt.authenticate(info);
|
||||
}else {
|
||||
launchApp();
|
||||
}
|
||||
}*/
|
||||
|
||||
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
|
||||
if(obj == null) return; // Cancelled
|
||||
@ -252,4 +287,5 @@ public class MainActivity extends AppCompatActivity {
|
||||
outState.putLong("pauseTime", pauseTime);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -11,12 +11,15 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.model.OTPData;
|
||||
import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter;
|
||||
import com.cringe_studios.cringe_authenticator.otplist.OTPListItem;
|
||||
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
|
||||
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.StyledDialogBuilder;
|
||||
import com.cringe_studios.cringe_authenticator_library.OTPException;
|
||||
@ -90,7 +93,13 @@ public class GroupFragment extends NamedFragment {
|
||||
break;
|
||||
case 2:
|
||||
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);
|
||||
saveOTPs();
|
||||
}, null);
|
||||
@ -113,20 +122,32 @@ public class GroupFragment extends NamedFragment {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private void loadOTPs() {
|
||||
List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupID);
|
||||
/*List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupID); TODO
|
||||
|
||||
for(OTPData otp : data) {
|
||||
otpListAdapter.add(otp);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public void addOTP(OTPData data) {
|
||||
SettingsUtil.addOTP(requireContext(), groupID, data);
|
||||
//SettingsUtil.addOTP(requireContext(), groupID, data); TODO
|
||||
otpListAdapter.add(data);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRON
|
||||
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -15,13 +16,21 @@ import androidx.annotation.Nullable;
|
||||
import androidx.biometric.BiometricManager;
|
||||
|
||||
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.util.DialogUtil;
|
||||
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 java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
public class SettingsFragment extends NamedFragment {
|
||||
|
||||
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.setOnCheckedChangeListener((view, checked) -> SettingsUtil.setEnableIntroVideo(requireContext(), checked));
|
||||
|
||||
|
@ -10,8 +10,11 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
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.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.SettingsUtil;
|
||||
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
|
||||
@ -61,7 +64,18 @@ public class URIHandlerActivity extends AppCompatActivity {
|
||||
|
||||
private void importCodes(OTPData... data) {
|
||||
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();
|
||||
}, this::finishAndRemoveTask);
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
package com.cringe_studios.cringe_authenticator.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
@ -260,4 +262,25 @@ public class DialogUtil {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -3,10 +3,8 @@ package com.cringe_studios.cringe_authenticator.util;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
@ -38,7 +36,7 @@ public class SettingsUtil {
|
||||
GROUPS_PREFS_NAME = "groups",
|
||||
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) {
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
@ -65,7 +63,7 @@ public class SettingsUtil {
|
||||
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", "[]");
|
||||
return Arrays.asList(GSON.fromJson(currentOTPs, OTPData[].class));
|
||||
}
|
||||
@ -85,6 +83,28 @@ public class SettingsUtil {
|
||||
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
.putString("group." + group + ".otps", GSON.toJson(otps.toArray(new OTPData[0])))
|
||||
.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) {
|
||||
|
@ -51,6 +51,7 @@
|
||||
<string name="qr_scanner_migration_part">Code %d of %d scanned</string>
|
||||
<string name="screen_security">Screen Security</string>
|
||||
<string name="hide_codes">Hide Codes</string>
|
||||
<string name="enable_encryption">Enable encryption</string>
|
||||
<string-array name="view_edit_move_delete">
|
||||
<item>View</item>
|
||||
<item>Edit</item>
|
||||
|
Loading…
Reference in New Issue
Block a user