mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-28 01:36:08 +00:00
Merge pull request #74 from alexbakker/feature-encrypted-andotp
Add support for importing encrypted andOTP backups
This commit is contained in:
commit
c2cb5a3044
5 changed files with 120 additions and 32 deletions
|
@ -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()];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
android:paddingEnd="20dp"
|
||||
android:paddingTop="20dp">
|
||||
<EditText
|
||||
android:id="@+id/text_groupname"
|
||||
android:hint="@string/group_name_hint"
|
||||
android:id="@+id/text_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
Loading…
Add table
Reference in a new issue