Crypto (WIP)
This commit is contained in:
parent
0025f37e99
commit
e80d63336d
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.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) {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user