diff --git a/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptoUtils.java b/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptoUtils.java index 0497d8ed..acc09667 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptoUtils.java +++ b/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptoUtils.java @@ -30,6 +30,7 @@ public class CryptoUtils { public static final String CRYPTO_AEAD = "AES/GCM/NoPadding"; public static final byte CRYPTO_AEAD_KEY_SIZE = 32; public static final byte CRYPTO_AEAD_TAG_SIZE = 16; + public static final byte CRYPTO_AEAD_NONCE_SIZE = 12; public static final int CRYPTO_SCRYPT_N = 1 << 15; public static final int CRYPTO_SCRYPT_r = 8; @@ -92,9 +93,14 @@ public class CryptoUtils { public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params) throws IOException, BadPaddingException, IllegalBlockSizeException { + return decrypt(encrypted, 0, encrypted.length, cipher, params); + } + + public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params) + throws IOException, BadPaddingException, IllegalBlockSizeException { // append the tag to the ciphertext ByteArrayOutputStream stream = new ByteArrayOutputStream(); - stream.write(encrypted); + stream.write(encrypted, encryptedOffset, encryptedLen); stream.write(params.getTag()); encrypted = stream.toByteArray(); @@ -124,7 +130,7 @@ public class CryptoUtils { return data; } - private static byte[] toBytes(char[] chars) { + public static byte[] toBytes(char[] chars) { CharBuffer charBuf = CharBuffer.wrap(chars); ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf); byte[] bytes = new byte[byteBuf.limit()]; diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java index 0999d3c5..8ef5017a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java +++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java @@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.importers; import android.content.Context; +import com.beemdevelopment.aegis.crypto.CryptParameters; +import com.beemdevelopment.aegis.crypto.CryptResult; +import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.db.DatabaseEntry; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.Base32Exception; @@ -10,6 +13,7 @@ import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.SteamInfo; import com.beemdevelopment.aegis.otp.TotpInfo; +import com.beemdevelopment.aegis.ui.Dialogs; import org.json.JSONArray; import org.json.JSONException; @@ -17,6 +21,18 @@ import org.json.JSONObject; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; public class AndOtpImporter extends DatabaseImporter { @@ -38,17 +54,78 @@ public class AndOtpImporter extends DatabaseImporter { byte[] bytes; try { bytes = reader.readAll(); - JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8)); - return new State(array); - } catch (IOException | JSONException e) { + } catch (IOException e) { throw new DatabaseImporterException(e); } + + try { + return read(bytes); + } catch (JSONException e) { + // andOTP doesn't have a proper way to indicate whether a file is encrypted + // so, if we can't parse it as JSON, we'll have to assume it is + return new EncryptedState(bytes); + } } - public static class State extends DatabaseImporter.State { + private static DecryptedState read(byte[] bytes) throws JSONException { + JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8)); + return new DecryptedState(array); + } + + public static class EncryptedState extends DatabaseImporter.State { + private byte[] _data; + + public EncryptedState(byte[] data) { + super(true); + _data = data; + } + + public DecryptedState decrypt(char[] password) throws DatabaseImporterException { + try { + // WARNING: DON'T DO THIS IN YOUR OWN CODE + // this exists solely to support encrypted andOTP backups + // it is not a secure way to derive a key from a password + MessageDigest hash = MessageDigest.getInstance("SHA-256"); + byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password)); + SecretKey key = new SecretKeySpec(keyBytes, "AES"); + + // extract nonce and tag + byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE); + byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length); + CryptParameters params = new CryptParameters(nonce, tag); + + Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce); + int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE; + int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE; + CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params); + return read(result.getData()); + } catch (IOException | BadPaddingException | JSONException e) { + throw new DatabaseImporterException(e); + } catch (NoSuchAlgorithmException + | InvalidAlgorithmParameterException + | InvalidKeyException + | NoSuchPaddingException + | IllegalBlockSizeException e) { + throw new RuntimeException(e); + } + } + + public void decrypt(Context context, DecryptListener listener) { + Dialogs.showPasswordInputDialog(context, password -> { + try { + DecryptedState state = decrypt(password); + listener.onStateDecrypted(state); + } catch (DatabaseImporterException e) { + listener.onError(e); + } + }); + } + } + + public static class DecryptedState extends DatabaseImporter.State { private JSONArray _obj; - private State(JSONArray obj) { + private DecryptedState(JSONArray obj) { super(false); _obj = obj; } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java index aa8dac6c..f52a535d 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java @@ -6,7 +6,9 @@ import android.content.Context; import android.content.DialogInterface; import android.hardware.fingerprint.FingerprintManager; import android.text.Editable; +import android.text.InputType; import android.text.TextWatcher; +import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.Button; @@ -52,17 +54,6 @@ public class Dialogs { dialog.show(); } - public static void showTextInputDialog(Context context, @StringRes int titleId, TextInputListener listener) { - EditText input = new EditText(context); - - showSecureDialog(new AlertDialog.Builder(context) - .setTitle(titleId) - .setView(input) - .setPositiveButton(android.R.string.ok, (dialog, which) -> - listener.onTextInputResult(input.getText().toString())) - .create()); - } - public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) { showSecureDialog(new AlertDialog.Builder(activity) .setTitle(activity.getString(R.string.delete_entry)) @@ -141,20 +132,34 @@ public class Dialogs { showSecureDialog(dialog); } - public static void showCreateNewGroupDialog(Activity activity, TextInputListener listener) { - View view = activity.getLayoutInflater().inflate(R.layout.dialog_newgroup, null); - EditText groupName = view.findViewById(R.id.text_groupname); + private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) { + View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null); + EditText input = view.findViewById(R.id.text_input); + if (isSecret) { + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + input.setHint(hintId); - AlertDialog dialog = new AlertDialog.Builder(activity) - .setTitle(R.string.set_group) + AlertDialog dialog = new AlertDialog.Builder(context) + .setTitle(titleId) .setView(view) - .setPositiveButton(android.R.string.ok, (dialog1, which) -> - listener.onTextInputResult(groupName.getText().toString())) + .setPositiveButton(android.R.string.ok, (dialog1, which) -> { + char[] text = EditTextHelper.getEditTextChars(input); + listener.onTextInputResult(text); + }) .create(); showSecureDialog(dialog); } + public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) { + showTextInputDialog(context, titleId, hintId, listener, false); + } + + public static void showPasswordInputDialog(Context context, TextInputListener listener) { + showTextInputDialog(context, R.string.set_password, R.string.password, listener, true); + } + public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) { View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null); NumberPicker numberPicker = view.findViewById(R.id.numberPicker); @@ -223,7 +228,7 @@ public class Dialogs { } public interface TextInputListener { - void onTextInputResult(String text); + void onTextInputResult(char[] text); } public interface SlotListener { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java index f5d0e489..48eac590 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -216,15 +216,16 @@ public class EditEntryActivity extends AegisActivity { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (position == _spinnerGroupList.size() - 1) { - Dialogs.showCreateNewGroupDialog(activity, text -> { - if (text.isEmpty()) { + Dialogs.showTextInputDialog(activity, R.string.set_group, R.string.group_name_hint, text -> { + String str = new String(text); + if (str.isEmpty()) { return; } - _groups.add(text); + _groups.add(str); // reset the selection to "No group" to work around a quirk _spinnerGroup.setSelection(0, false); updateGroupSpinnerList(); - _spinnerGroup.setSelection(_spinnerGroupList.indexOf(text), false); + _spinnerGroup.setSelection(_spinnerGroupList.indexOf(str), false); }); _spinnerGroup.setSelection(prevPosition, false); } else { diff --git a/app/src/main/res/layout/dialog_newgroup.xml b/app/src/main/res/layout/dialog_text_input.xml similarity index 83% rename from app/src/main/res/layout/dialog_newgroup.xml rename to app/src/main/res/layout/dialog_text_input.xml index 2aefb394..ba54be9d 100644 --- a/app/src/main/res/layout/dialog_newgroup.xml +++ b/app/src/main/res/layout/dialog_text_input.xml @@ -7,8 +7,7 @@ android:paddingEnd="20dp" android:paddingTop="20dp">