diff --git a/app/build.gradle b/app/build.gradle
index 12aebeb0..0be15093 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,8 +19,9 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 29
- versionCode 28
+ versionCode 29
versionName "1.1.4"
+ multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
}
@@ -72,6 +73,7 @@ dependencies {
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.google.guava:guava:28.2-android'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:5.1.0'
implementation 'com.github.avito-tech:krop:0.44'
@@ -91,5 +93,6 @@ dependencies {
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
+ testImplementation 'com.google.guava:guava:28.2-jre"'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptParameters.java b/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptParameters.java
index 9b6a749e..3f1a55b0 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptParameters.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/crypto/CryptParameters.java
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.crypto;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
-import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
return obj;
}
- public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
+ public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
byte[] nonce = Hex.decode(obj.getString("nonce"));
byte[] tag = Hex.decode(obj.getString("tag"));
return new CryptParameters(nonce, tag);
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java
index 2f5a72f3..29c0f89a 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java
@@ -1,155 +1,23 @@
package com.beemdevelopment.aegis.encoding;
-// modified for use in Aegis
+import com.google.common.io.BaseEncoding;
-/* (PD) 2001 The Bitzi Corporation
- * Please see http://bitzi.com/publicdomain for more info.
- *
- * As modified by Patrick Woodworth:
- *
- * Copyright 2011 Patrick Woodworth
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import java.util.Arrays;
-
-/**
- * Base32 - encodes and decodes RFC3548 Base32
- * (see http://www.faqs.org/rfcs/rfc3548.html )
- *
- * @author Robert Kaye
- * @author Gordon Mohr
- */
public class Base32 {
- private static final String base32Chars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
- private static final int[] base32Lookup =
- { 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
- 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
- 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
- 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
- 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
- 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
- 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
- 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
- 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
- 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
- };
-
- /**
- * Encodes byte array to Base32 String.
- *
- * @param bytes Bytes to encode.
- * @return Encoded byte array bytes
as a String.
- *
- */
- public static char[] encode(final byte[] bytes) {
- int i = 0, index = 0, digit = 0, j = 0;
- int currByte, nextByte;
- char[] base32 = new char[(bytes.length + 7) * 8 / 5];
-
- while (i < bytes.length) {
- currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
+ private static final BaseEncoding _encoding = BaseEncoding.base32().omitPadding();
- /* Is the current digit going to span a byte boundary? */
- if (index > 3) {
- if ((i + 1) < bytes.length) {
- nextByte =
- (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
- } else {
- nextByte = 0;
- }
+ private Base32() {
- digit = currByte & (0xFF >> index);
- index = (index + 5) % 8;
- digit <<= index;
- digit |= nextByte >> (8 - index);
- i++;
- } else {
- digit = (currByte >> (8 - (index + 5))) & 0x1F;
- index = (index + 5) % 8;
- if (index == 0)
- i++;
- }
- base32[j++] = base32Chars.charAt(digit);
- }
-
- return Arrays.copyOf(base32, j);
}
- /**
- * Decodes the given Base32 String to a raw byte array.
- *
- * @param base32
- * @return Decoded base32
String as a raw byte array.
- */
- public static byte[] decode(final char[] base32) throws Base32Exception {
- int i, index, lookup, offset, digit;
- byte[] bytes = new byte[base32.length * 5 / 8];
-
- for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
- // stop decoding when a padding char is encountered
- if (base32[i] == '=') {
- // make sure the rest is also padding, but don't bother verifying the length
- for (int j = i + 1; j < base32.length; j++) {
- if (base32[j] != '=') {
- throw new Base32Exception("bad padding");
- }
- }
- break;
- }
-
- lookup = base32[i] - '0';
- digit = decodeDigit(lookup);
-
- if (index <= 3) {
- index = (index + 5) % 8;
- if (index == 0) {
- bytes[offset] |= digit;
- offset++;
- if (offset >= bytes.length)
- break;
- } else {
- bytes[offset] |= digit << (8 - index);
- }
- } else {
- index = (index + 5) % 8;
- bytes[offset] |= (digit >>> index);
- offset++;
-
- if (offset >= bytes.length) {
- break;
- }
- bytes[offset] |= digit << (8 - index);
- }
+ public static byte[] decode(String s) throws EncodingException {
+ try {
+ return _encoding.decode(s.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new EncodingException(e);
}
- return bytes;
}
- private static int decodeDigit(int c) throws Base32Exception {
- /* Skip chars outside the lookup table */
- if (c < 0 || c >= base32Lookup.length) {
- throw new Base32Exception("char not found in base32 lookup table");
- }
-
- int digit = base32Lookup[c];
-
- /* If this digit is not in the table, ignore it */
- if (digit == 0xFF) {
- throw new Base32Exception("char not found in base32 lookup table");
- }
-
- return digit;
+ public static String encode(byte[] data) {
+ return _encoding.encode(data);
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32Exception.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32Exception.java
deleted file mode 100644
index 63a21022..00000000
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32Exception.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.beemdevelopment.aegis.encoding;
-
-public class Base32Exception extends Exception {
- public Base32Exception(String message) {
- super(message);
- }
-}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64.java
index 09966972..b6dbe4b9 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64.java
@@ -1,24 +1,27 @@
package com.beemdevelopment.aegis.encoding;
+import com.google.common.io.BaseEncoding;
+
import java.nio.charset.StandardCharsets;
public class Base64 {
- private static final int _flags = android.util.Base64.NO_WRAP;
-
private Base64() {
}
- public static byte[] decode(String s) throws Base64Exception {
+ public static byte[] decode(String s) throws EncodingException {
try {
- return android.util.Base64.decode(s, _flags);
+ return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException e) {
- throw new Base64Exception(e);
+ throw new EncodingException(e);
}
}
+ public static byte[] decode(byte[] s) throws EncodingException {
+ return decode(new String(s, StandardCharsets.UTF_8));
+ }
+
public static String encode(byte[] data) {
- byte[] encoded = android.util.Base64.encode(data, _flags);
- return new String(encoded, StandardCharsets.UTF_8);
+ return BaseEncoding.base64().encode(data);
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64Exception.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64Exception.java
deleted file mode 100644
index 09219bf7..00000000
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base64Exception.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.beemdevelopment.aegis.encoding;
-
-public class Base64Exception extends Exception {
- public Base64Exception(Throwable cause) {
- super(cause);
- }
-}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java
new file mode 100644
index 00000000..ac75a347
--- /dev/null
+++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java
@@ -0,0 +1,9 @@
+package com.beemdevelopment.aegis.encoding;
+
+import java.io.IOException;
+
+public class EncodingException extends IOException {
+ public EncodingException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Hex.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Hex.java
index de96e015..db8ff046 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Hex.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/Hex.java
@@ -1,46 +1,21 @@
package com.beemdevelopment.aegis.encoding;
-// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
-// It is licensed under GPLv2 with a classpath exception.
+import com.google.common.io.BaseEncoding;
+
public class Hex {
private Hex() {
- }
- private static int hexToBin(char ch) {
- if ('0' <= ch && ch <= '9') return ch - '0';
- if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
- if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
- return -1;
}
- private static final char[] hexCode = "0123456789abcdef".toCharArray();
-
- public static byte[] decode(String s) throws HexException {
- final int len = s.length();
-
- if (len % 2 != 0)
- throw new HexException("hexBinary needs to be even-length: " + s);
-
- byte[] out = new byte[len / 2];
-
- for (int i = 0; i < len; i += 2) {
- int h = hexToBin(s.charAt(i));
- int l = hexToBin(s.charAt(i + 1));
- if (h == -1 || l == -1)
- throw new HexException("contains illegal character for hexBinary: " + s);
-
- out[i / 2] = (byte) (h * 16 + l);
+ public static byte[] decode(String s) throws EncodingException {
+ try {
+ return BaseEncoding.base16().decode(s.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new EncodingException(e);
}
-
- return out;
}
public static String encode(byte[] data) {
- StringBuilder r = new StringBuilder(data.length * 2);
- for (byte b : data) {
- r.append(hexCode[(b >> 4) & 0xF]);
- r.append(hexCode[(b & 0xF)]);
- }
- return r.toString();
+ return BaseEncoding.base16().lowerCase().encode(data);
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/HexException.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/HexException.java
deleted file mode 100644
index 12d9782c..00000000
--- a/app/src/main/java/com/beemdevelopment/aegis/encoding/HexException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.beemdevelopment.aegis.encoding;
-
-public class HexException extends Exception {
- public HexException(String message) {
- super(message);
- }
-}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java
index 7cff6531..4df7a83a 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java
@@ -2,13 +2,13 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
+import com.beemdevelopment.aegis.encoding.EncodingException;
+import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
-import com.beemdevelopment.aegis.encoding.Base64Exception;
-import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
@@ -102,7 +102,7 @@ public class AegisImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
- } catch (JSONException | OtpInfoException | Base64Exception e) {
+ } catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
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 19f576e4..5df1ce51 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java
@@ -10,7 +10,7 @@ import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -201,7 +201,7 @@ public class AndOtpImporter extends DatabaseImporter {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
- byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
+ byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
@@ -230,7 +230,7 @@ public class AndOtpImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, issuer);
- } catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
+ } catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java
index ef3fec8f..8cccafa5 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java
@@ -3,13 +3,16 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
-import com.beemdevelopment.aegis.vault.VaultEntry;
+import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.Base64;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
+import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.util.PreferenceParser;
+import com.beemdevelopment.aegis.vault.VaultEntry;
import org.json.JSONArray;
import org.json.JSONException;
@@ -18,11 +21,33 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _pkgName = "com.authy.authy";
+ private static final int ITERATIONS = 1000;
+ private static final int KEY_SIZE = 256;
+ private static final byte[] IV = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+
public AuthyImporter(Context context) {
super(context);
}
@@ -52,18 +77,76 @@ public class AuthyImporter extends DatabaseImporter {
}
}
- return new State(array);
+ for (int i = 0; i < array.length(); i++) {
+ if (!array.getJSONObject(i).has("decryptedSecret")) {
+ return new EncryptedState(array);
+ }
+ }
+
+ return new DecryptedState(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
- public static class State extends DatabaseImporter.State {
- private JSONArray _obj;
+ public static class EncryptedState extends DatabaseImporter.State {
+ private JSONArray _array;
+
+ private EncryptedState(JSONArray array) {
+ super(true);
+ _array = array;
+ }
+
+ @Override
+ public void decrypt(Context context, DecryptListener listener) {
+ Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
+ try {
+ for (int i = 0; i < _array.length(); i++) {
+ JSONObject obj = _array.getJSONObject(i);
+ String secretString = obj.optString("encryptedSecret", null);
+ if (secretString == null) {
+ continue;
+ }
+
+ byte[] encryptedSecret = Base64.decode(secretString);
+ byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
+ SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
+ SecretKey key = factory.generateSecret(spec);
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
+ IvParameterSpec ivSpec = new IvParameterSpec(IV);
+ cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
+
+ byte[] secret = cipher.doFinal(encryptedSecret);
+ obj.remove("encryptedSecret");
+ obj.remove("salt");
+ obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
+ }
+
+ DecryptedState state = new DecryptedState(_array);
+ listener.onStateDecrypted(state);
+ } catch (JSONException
+ | EncodingException
+ | NoSuchAlgorithmException
+ | InvalidKeySpecException
+ | InvalidAlgorithmParameterException
+ | InvalidKeyException
+ | NoSuchPaddingException
+ | BadPaddingException
+ | IllegalBlockSizeException e) {
+ listener.onError(e);
+ }
+ });
+ }
+ }
+
+ public static class DecryptedState extends DatabaseImporter.State {
+ private JSONArray _array;
- private State(JSONArray obj) {
+ private DecryptedState(JSONArray array) {
super(false);
- _obj = obj;
+ _array = array;
}
@Override
@@ -71,8 +154,8 @@ public class AuthyImporter extends DatabaseImporter {
Result result = new Result();
try {
- for (int i = 0; i < _obj.length(); i++) {
- JSONObject entryObj = _obj.getJSONObject(i);
+ for (int i = 0; i < _array.length(); i++) {
+ JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
@@ -90,42 +173,46 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
- authyEntryInfo.OriginalName = entry.getString("originalName");
+ authyEntryInfo.OriginalName = entry.optString("originalName", null);
+ authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
int digits = entry.getInt("digits");
- byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
+ byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
- } catch (OtpInfoException | JSONException | Base32Exception e) {
+ } catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
- String seperator = "";
+ String separator = "";
- if (info.OriginalName.contains(":")) {
+ if (info.OriginalIssuer != null) {
+ info.Issuer = info.OriginalIssuer;
+ } else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
- seperator = ":";
+ separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
- seperator = " - ";
+ separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
- info.Name = info.Name.replace(info.Issuer + seperator, "");
+ info.Name = info.Name.replace(info.Issuer + separator, "");
}
}
private static class AuthyEntryInfo {
String OriginalName;
+ String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
index f72711bb..494422c2 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
@@ -32,7 +32,9 @@ public abstract class DatabaseImporter {
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
+ _importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
+ _importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
@@ -40,7 +42,9 @@ public abstract class DatabaseImporter {
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
+ _appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
+ _appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
}
public DatabaseImporter(Context context) {
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterEntryException.java b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterEntryException.java
index 7aff6df4..9df0be86 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterEntryException.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterEntryException.java
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
+ public DatabaseImporterEntryException(String message, String text) {
+ super(message);
+ _text = text;
+ }
+
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java
index fc1e4169..c4d8377e 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java
@@ -2,26 +2,17 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteException;
-import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
-import com.topjohnwu.superuser.ShellUtils;
+import com.beemdevelopment.aegis.vault.VaultEntry;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
import java.util.List;
-import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
-
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@@ -45,37 +36,9 @@ public class GoogleAuthImporter extends DatabaseImporter {
@Override
public State read(FileReader reader) throws DatabaseImporterException {
- File file;
-
- try {
- // create a temporary copy of the database so that SQLiteDatabase can open it
- file = File.createTempFile("google-import-", "", getContext().getCacheDir());
- try (FileOutputStream out = new FileOutputStream(file)) {
- ShellUtils.pump(reader.getStream(), out);
- }
- } catch (IOException e) {
- throw new DatabaseImporterException(e);
- }
-
- try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
- try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
- List entries = new ArrayList<>();
-
- if (cursor.moveToFirst()) {
- do {
- Entry entry = new Entry(cursor);
- entries.add(entry);
- } while(cursor.moveToNext());
- }
-
- return new State(entries);
- }
- } catch (SQLiteException e) {
- throw new DatabaseImporterException(e);
- } finally {
- // always delete the temporary file
- file.delete();
- }
+ SqlImporterHelper helper = new SqlImporterHelper(getContext());
+ List entries = helper.read(Entry.class, reader.getStream(), "accounts");
+ return new State(entries);
}
public static class State extends DatabaseImporter.State {
@@ -104,7 +67,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
- byte[] secret = Base32.decode(entry.getSecret().toCharArray());
+ byte[] secret = Base32.decode(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
@@ -125,33 +88,13 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, entry.getIssuer());
- } catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
+ } catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
- private static String getString(Cursor cursor, String columnName) {
- return getString(cursor, columnName, null);
- }
-
- private static String getString(Cursor cursor, String columnName, String def) {
- String res = cursor.getString(cursor.getColumnIndex(columnName));
- if (res == null) {
- return def;
- }
- return res;
- }
-
- private static int getInt(Cursor cursor, String columnName) {
- return cursor.getInt(cursor.getColumnIndex(columnName));
- }
-
- private static long getLong(Cursor cursor, String columnName) {
- return cursor.getLong(cursor.getColumnIndex(columnName));
- }
-
- private static class Entry {
+ private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _email;
@@ -159,11 +102,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
private long _counter;
public Entry(Cursor cursor) {
- _type = getInt(cursor, "type");
- _secret = getString(cursor, "secret");
- _email = getString(cursor, "email", "");
- _issuer = getString(cursor, "issuer", "");
- _counter = getLong(cursor, "counter");
+ super(cursor);
+ _type = SqlImporterHelper.getInt(cursor, "type");
+ _secret = SqlImporterHelper.getString(cursor, "secret");
+ _email = SqlImporterHelper.getString(cursor, "email", "");
+ _issuer = SqlImporterHelper.getString(cursor, "issuer", "");
+ _counter = SqlImporterHelper.getLong(cursor, "counter");
}
public int getType() {
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/MicrosoftAuthImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/MicrosoftAuthImporter.java
new file mode 100644
index 00000000..33af0eae
--- /dev/null
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/MicrosoftAuthImporter.java
@@ -0,0 +1,126 @@
+package com.beemdevelopment.aegis.importers;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import com.beemdevelopment.aegis.encoding.Base32;
+import com.beemdevelopment.aegis.encoding.Base64;
+import com.beemdevelopment.aegis.encoding.EncodingException;
+import com.beemdevelopment.aegis.otp.OtpInfo;
+import com.beemdevelopment.aegis.otp.OtpInfoException;
+import com.beemdevelopment.aegis.otp.TotpInfo;
+import com.beemdevelopment.aegis.vault.VaultEntry;
+
+import java.util.List;
+
+public class MicrosoftAuthImporter extends DatabaseImporter {
+ private static final String _subPath = "databases/PhoneFactor";
+ private static final String _pkgName = "com.azure.authenticator";
+
+ private static final int TYPE_TOTP = 0;
+ private static final int TYPE_MICROSOFT = 1;
+
+ public MicrosoftAuthImporter(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected String getAppPkgName() {
+ return _pkgName;
+ }
+
+ @Override
+ protected String getAppSubPath() {
+ return _subPath;
+ }
+
+ @Override
+ public State read(FileReader reader) throws DatabaseImporterException {
+ SqlImporterHelper helper = new SqlImporterHelper(getContext());
+ List entries = helper.read(Entry.class, reader.getStream(), "accounts");
+ return new State(entries);
+ }
+
+ public static class State extends DatabaseImporter.State {
+ private List _entries;
+
+ private State(List entries) {
+ super(false);
+ _entries = entries;
+ }
+
+ @Override
+ public Result convert() {
+ Result result = new Result();
+
+ for (Entry sqlEntry : _entries) {
+ try {
+ int type = sqlEntry.getType();
+ if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
+ VaultEntry entry = convertEntry(sqlEntry);
+ result.addEntry(entry);
+ }
+ } catch (DatabaseImporterEntryException e) {
+ result.addError(e);
+ }
+ }
+
+ return result;
+ }
+
+ private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
+ try {
+ byte[] secret;
+ int digits = 6;
+
+ switch (entry.getType()) {
+ case TYPE_TOTP:
+ secret = Base32.decode(entry.getSecret());
+ break;
+ case TYPE_MICROSOFT:
+ digits = 8;
+ secret = Base64.decode(entry.getSecret());
+ break;
+ default:
+ throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
+ }
+
+ OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
+ return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
+ } catch (EncodingException | OtpInfoException e) {
+ throw new DatabaseImporterEntryException(e, entry.toString());
+ }
+ }
+ }
+
+ private static class Entry extends SqlImporterHelper.Entry {
+ private int _type;
+ private String _secret;
+ private String _issuer;
+ private String _userName;
+
+ public Entry(Cursor cursor) {
+ super(cursor);
+ _type = SqlImporterHelper.getInt(cursor, "account_type");
+ _secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
+ _issuer = SqlImporterHelper.getString(cursor, "name");
+ _userName = SqlImporterHelper.getString(cursor, "username");
+ }
+
+ public int getType() {
+ return _type;
+ }
+
+ public String getSecret() {
+ return _secret;
+ }
+
+ public String getIssuer() {
+ return _issuer;
+ }
+
+ public String getUserName() {
+ return _userName;
+ }
+ }
+}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java b/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java
new file mode 100644
index 00000000..15abc0c1
--- /dev/null
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java
@@ -0,0 +1,89 @@
+package com.beemdevelopment.aegis.importers;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+import com.topjohnwu.superuser.ShellUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
+
+public class SqlImporterHelper {
+ private Context _context;
+
+ public SqlImporterHelper(Context context) {
+ _context = context;
+ }
+
+ public List read(Class type, InputStream inStream, String table) throws DatabaseImporterException {
+ File file;
+
+ try {
+ // create a temporary copy of the database so that SQLiteDatabase can open it
+ file = File.createTempFile("db-import-", "", _context.getCacheDir());
+ try (FileOutputStream out = new FileOutputStream(file)) {
+ ShellUtils.pump(inStream, out);
+ }
+ } catch (IOException e) {
+ throw new DatabaseImporterException(e);
+ }
+
+ try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
+ try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
+ List entries = new ArrayList<>();
+
+ if (cursor.moveToFirst()) {
+ do {
+ T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
+ entries.add(entry);
+ } while (cursor.moveToNext());
+ }
+
+ return entries;
+ } catch (InstantiationException | IllegalAccessException
+ | NoSuchMethodException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ } catch (SQLiteException e) {
+ throw new DatabaseImporterException(e);
+ } finally {
+ // always delete the temporary file
+ file.delete();
+ }
+ }
+
+ public static String getString(Cursor cursor, String columnName) {
+ return cursor.getString(cursor.getColumnIndex(columnName));
+ }
+
+ public static String getString(Cursor cursor, String columnName, String def) {
+ String res = cursor.getString(cursor.getColumnIndex(columnName));
+ if (res == null) {
+ return def;
+ }
+ return res;
+ }
+
+ public static int getInt(Cursor cursor, String columnName) {
+ return cursor.getInt(cursor.getColumnIndex(columnName));
+ }
+
+ public static long getLong(Cursor cursor, String columnName) {
+ return cursor.getLong(cursor.getColumnIndex(columnName));
+ }
+
+ public static abstract class Entry {
+ public Entry (Cursor cursor) {
+
+ }
+ }
+}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java
index 9cf3c921..36a91ebf 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java
@@ -3,9 +3,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base64;
-import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
@@ -82,7 +82,7 @@ public class SteamImporter extends DatabaseImporter {
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
- } catch (JSONException | Base64Exception | OtpInfoException e) {
+ } catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/importers/TotpAuthenticatorImporter.java b/app/src/main/java/com/beemdevelopment/aegis/importers/TotpAuthenticatorImporter.java
new file mode 100644
index 00000000..cd21e3d2
--- /dev/null
+++ b/app/src/main/java/com/beemdevelopment/aegis/importers/TotpAuthenticatorImporter.java
@@ -0,0 +1,235 @@
+package com.beemdevelopment.aegis.importers;
+
+import android.content.Context;
+import android.util.Xml;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.beemdevelopment.aegis.R;
+import com.beemdevelopment.aegis.crypto.CryptoUtils;
+import com.beemdevelopment.aegis.encoding.Base32;
+import com.beemdevelopment.aegis.encoding.Base64;
+import com.beemdevelopment.aegis.encoding.EncodingException;
+import com.beemdevelopment.aegis.encoding.Hex;
+import com.beemdevelopment.aegis.otp.OtpInfoException;
+import com.beemdevelopment.aegis.otp.TotpInfo;
+import com.beemdevelopment.aegis.ui.Dialogs;
+import com.beemdevelopment.aegis.util.PreferenceParser;
+import com.beemdevelopment.aegis.vault.VaultEntry;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+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.ArrayList;
+import java.util.List;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class TotpAuthenticatorImporter extends DatabaseImporter {
+ private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
+ private static final String _pkgName = "com.authenticator.authservice2";
+
+ // WARNING: DON'T DO THIS IN YOUR OWN CODE
+ // this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
+ private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
+ private static final byte[] IV = new byte[]{
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ };
+
+ private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
+
+ public TotpAuthenticatorImporter(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected String getAppPkgName() {
+ return _pkgName;
+ }
+
+ @Override
+ protected String getAppSubPath() {
+ return _subPath;
+ }
+
+ @Override
+ public State read(FileReader reader) throws DatabaseImporterException {
+ try {
+ if (reader.isInternal()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
+ parser.setInput(reader.getStream(), null);
+ parser.nextTag();
+
+ String data = null;
+ for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
+ if (entry.Name.equals(PREF_KEY)) {
+ data = entry.Value;
+ }
+ }
+
+ if (data == null) {
+ throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
+ }
+
+ List entries = parse(data);
+ return new DecryptedState(entries);
+ } else {
+ byte[] base64 = reader.readAll();
+ byte[] cipherText = Base64.decode(base64);
+ return new EncryptedState(cipherText);
+ }
+ } catch (IOException | XmlPullParserException | JSONException e) {
+ throw new DatabaseImporterException(e);
+ }
+ }
+
+ private static List parse(String data) throws JSONException {
+ JSONArray array = new JSONArray(data);
+
+ List entries = new ArrayList<>();
+ for (int i = 0; i < array.length(); ++i) {
+ String s = array.getString(i);
+ entries.add(new JSONObject(s));
+ }
+
+ return entries;
+ }
+
+ public static class EncryptedState extends DatabaseImporter.State {
+ private byte[] _data;
+
+ public EncryptedState(byte[] data) {
+ super(true);
+ _data = data;
+ }
+
+ private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
+ try {
+ // WARNING: DON'T DO THIS IN YOUR OWN CODE
+ // this 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");
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
+ IvParameterSpec spec = new IvParameterSpec(IV);
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+
+ byte[] bytes = cipher.doFinal(_data);
+ JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
+ JSONArray keys = obj.names();
+
+ List entries = new ArrayList<>();
+ if (keys != null && keys.length() > 0) {
+ entries = parse((String) keys.get(0));
+ }
+
+ return new DecryptedState(entries);
+ } catch (NoSuchAlgorithmException
+ | NoSuchPaddingException
+ | InvalidAlgorithmParameterException
+ | InvalidKeyException
+ | BadPaddingException
+ | IllegalBlockSizeException
+ | JSONException e) {
+ throw new DatabaseImporterException(e);
+ }
+ }
+
+ @Override
+ public void decrypt(Context context, DecryptListener listener) {
+ Dialogs.showSecureDialog(new AlertDialog.Builder(context)
+ .setMessage(R.string.choose_totpauth_importer)
+ .setPositiveButton(R.string.yes, (dialog, which) -> {
+ Dialogs.showPasswordInputDialog(context, password -> {
+ decrypt(password, listener);
+ });
+ })
+ .setNegativeButton(R.string.no, (dialog, which) -> {
+ decrypt(PASSWORD, listener);
+ })
+ .create());
+ }
+
+ private void decrypt(char[] password, DecryptListener listener) {
+ try {
+ DecryptedState state = decrypt(password);
+ listener.onStateDecrypted(state);
+ } catch (DatabaseImporterException e) {
+ listener.onError(e);
+ }
+ }
+ }
+
+ public static class DecryptedState extends DatabaseImporter.State {
+ private List _objs;
+
+ private DecryptedState(List objs) {
+ super(false);
+ _objs = objs;
+ }
+
+ @Override
+ public Result convert() {
+ Result result = new Result();
+
+ for (JSONObject obj : _objs) {
+ try {
+ VaultEntry entry = convertEntry(obj);
+ result.addEntry(entry);
+ } catch (DatabaseImporterEntryException e) {
+ result.addError(e);
+ }
+ }
+
+ return result;
+ }
+
+ private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
+ try {
+ int base = obj.getInt("base");
+ String secretString = obj.getString("key");
+
+ byte[] secret;
+ switch (base) {
+ case 16:
+ secret = Hex.decode(secretString);
+ break;
+ case 32:
+ secret = Base32.decode(secretString);
+ break;
+ case 64:
+ secret = Base64.decode(secretString);
+ break;
+ default:
+ throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
+ }
+
+ TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
+ String name = obj.optString("name");
+ String issuer = obj.optString("issuer");
+
+ return new VaultEntry(info, name, issuer);
+ } catch (JSONException | OtpInfoException | EncodingException e) {
+ throw new DatabaseImporterEntryException(e, obj.toString());
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java
index 5e25a9ff..7a522c4d 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java
@@ -3,7 +3,7 @@ package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
public class GoogleAuthInfo {
private OtpInfo _info;
@@ -71,8 +71,8 @@ public class GoogleAuthInfo {
// decode secret
byte[] secret;
try {
- secret = Base32.decode(encodedSecret.toCharArray());
- } catch (Base32Exception e) {
+ secret = Base32.decode(encodedSecret);
+ } catch (EncodingException e) {
throw new GoogleAuthInfoException("bad secret", e);
}
@@ -101,7 +101,7 @@ public class GoogleAuthInfo {
default:
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
}
- } catch (OtpInfoException e) {
+ } catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
}
@@ -141,7 +141,7 @@ public class GoogleAuthInfo {
if (digits != null) {
info.setDigits(Integer.parseInt(digits));
}
- } catch (OtpInfoException e) {
+ } catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java
index 498e2681..5c77411f 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -93,7 +93,7 @@ public abstract class OtpInfo implements Serializable {
OtpInfo info;
try {
- byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
+ byte[] secret = Base32.decode(obj.getString("secret"));
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
@@ -110,7 +110,7 @@ public abstract class OtpInfo implements Serializable {
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
- } catch (Base32Exception | JSONException e) {
+ } catch (EncodingException | JSONException e) {
throw new OtpInfoException(e);
}
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 3731e2ef..3a8a0c52 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/ui/Dialogs.java
@@ -156,7 +156,7 @@ public class Dialogs {
showSecureDialog(dialog);
}
- private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
+ private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @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) {
@@ -164,18 +164,25 @@ public class Dialogs {
}
input.setHint(hintId);
- AlertDialog dialog = new AlertDialog.Builder(context)
+ AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
- char[] text = EditTextHelper.getEditTextChars(input);
- listener.onTextInputResult(text);
- })
- .create();
+ char[] text = EditTextHelper.getEditTextChars(input);
+ listener.onTextInputResult(text);
+ });
+ if (messageId != 0) {
+ builder.setMessage(messageId);
+ }
+ AlertDialog dialog = builder.create();
showSecureDialog(dialog);
}
+ private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
+ showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
+ }
+
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
}
@@ -184,6 +191,10 @@ public class Dialogs {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
+ public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
+ showTextInputDialog(context, R.string.set_password, messageId, 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);
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 1bf58044..bf3f28ca 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java
@@ -22,12 +22,18 @@ import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TableRow;
+import androidx.annotation.ArrayRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AlertDialog;
+
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
-import com.beemdevelopment.aegis.encoding.Base32Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
@@ -36,11 +42,11 @@ 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.util.Cloner;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
-import com.beemdevelopment.aegis.util.Cloner;
import java.io.ByteArrayOutputStream;
import java.text.Collator;
@@ -49,11 +55,6 @@ import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
-import androidx.annotation.ArrayRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
public class EditEntryActivity extends AegisActivity {
@@ -163,8 +164,8 @@ public class EditEntryActivity extends AegisActivity {
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
- char[] secretChars = Base32.encode(secretBytes);
- _textSecret.setText(secretChars, 0, secretChars.length);
+ String secretString = Base32.encode(secretBytes);
+ _textSecret.setText(secretString);
}
String type = _origEntry.getInfo().getType();
@@ -458,8 +459,11 @@ public class EditEntryActivity extends AegisActivity {
byte[] secret;
try {
- secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
- } catch (Base32Exception e) {
+ secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
+ if (secret.length == 0) {
+ throw new ParseException("Secret cannot be empty");
+ }
+ } catch (EncodingException e) {
throw new ParseException("Secret is not valid base32.");
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java
index 1abf82ad..4776ddbd 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java
@@ -108,7 +108,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
public void handleResult(Result rawResult) {
try {
// parse google auth uri
- GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
+ GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText().trim());
VaultEntry entry = new VaultEntry(info);
Intent intent = new Intent();
diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java
index 99ec9202..99b90732 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/vault/Vault.java
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis.vault;
-import com.beemdevelopment.aegis.encoding.Base64Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.UUIDMap;
@@ -43,7 +43,7 @@ public class Vault {
VaultEntry entry = VaultEntry.fromJson(array.getJSONObject(i));
entries.add(entry);
}
- } catch (Base64Exception | OtpInfoException | JSONException e) {
+ } catch (EncodingException | OtpInfoException | JSONException e) {
throw new VaultException(e);
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java
index ceba2518..a610e1e4 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultEntry.java
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.encoding.Base64;
-import com.beemdevelopment.aegis.encoding.Base64Exception;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -60,7 +60,7 @@ public class VaultEntry extends UUIDMap.Value {
return obj;
}
- public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
+ public static VaultEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, EncodingException {
// if there is no uuid, generate a new one
UUID uuid;
if (!obj.has("uuid")) {
diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java
index 66f4a331..ba68f08d 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultFile.java
@@ -3,11 +3,10 @@ package com.beemdevelopment.aegis.vault;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
+import com.beemdevelopment.aegis.encoding.Base64;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.vault.slots.SlotListException;
-import com.beemdevelopment.aegis.encoding.Base64;
-import com.beemdevelopment.aegis.encoding.Base64Exception;
-import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -95,7 +94,7 @@ public class VaultFile {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
- } catch (MasterKeyException | JSONException | Base64Exception e) {
+ } catch (MasterKeyException | JSONException | EncodingException e) {
throw new VaultFileException(e);
}
}
@@ -136,7 +135,7 @@ public class VaultFile {
SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
return new Header(slots, params);
- } catch (SlotListException | JSONException | HexException e) {
+ } catch (SlotListException | JSONException | EncodingException e) {
throw new VaultFileException(e);
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java
index 1d4ec33a..b378ad6c 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/vault/VaultManager.java
@@ -3,13 +3,13 @@ package com.beemdevelopment.aegis.vault;
import android.content.Context;
import android.content.Intent;
+import androidx.core.util.AtomicFile;
+
import com.beemdevelopment.aegis.services.NotificationService;
import org.json.JSONObject;
-import java.io.DataInputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -41,12 +41,9 @@ public class VaultManager {
public void load() throws VaultManagerException {
assertState(true, false);
- try (FileInputStream file = _context.openFileInput(FILENAME)) {
- byte[] fileBytes = new byte[(int) file.getChannel().size()];
- DataInputStream stream = new DataInputStream(file);
- stream.readFully(fileBytes);
- stream.close();
-
+ AtomicFile file = new AtomicFile(new File(_context.getFilesDir(), FILENAME));
+ try {
+ byte[] fileBytes = file.readFully();
_file = VaultFile.fromBytes(fileBytes);
_encrypt = _file.isEncrypted();
if (!isEncryptionEnabled()) {
@@ -77,11 +74,19 @@ public class VaultManager {
}
}
- public static void save(Context context, VaultFile file) throws VaultManagerException {
- byte[] bytes = file.toBytes();
- try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
+ public static void save(Context context, VaultFile vaultFile) throws VaultManagerException {
+ byte[] bytes = vaultFile.toBytes();
+ AtomicFile file = new AtomicFile(new File(context.getFilesDir(), FILENAME));
+
+ FileOutputStream stream = null;
+ try {
+ stream = file.startWrite();
stream.write(bytes);
+ file.finishWrite(stream);
} catch (IOException e) {
+ if (stream != null) {
+ file.failWrite(stream);
+ }
throw new VaultManagerException(e);
}
}
diff --git a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java
index 0c29982a..c0f35343 100644
--- a/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java
+++ b/app/src/main/java/com/beemdevelopment/aegis/vault/slots/Slot.java
@@ -5,8 +5,8 @@ import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
-import com.beemdevelopment.aegis.encoding.HexException;
import com.beemdevelopment.aegis.util.UUIDMap;
import org.json.JSONException;
@@ -144,7 +144,7 @@ public abstract class Slot extends UUIDMap.Value {
default:
throw new SlotException("unrecognized slot type");
}
- } catch (JSONException | HexException e) {
+ } catch (JSONException | EncodingException e) {
throw new SlotException(e);
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ada120c2..5499c21f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -66,7 +66,10 @@
Please confirm the password
A change in your device\'s security settings has been detected. Please go to \"Aegis -> Settings -> Biometrics\" and re-enable biometric unlock.
It\'s been a while since you\'ve entered your password. Do you still remember it?
+ It looks like your Authy tokens are encrypted. Please close Aegis, open Authy and unlock the tokens with your password. Instead, Aegis can also attempt to decrypt your Authy tokens for you, if you enter your password below.
+ Yes
+ No
Unlock
Biometrics
Advanced
@@ -122,6 +125,7 @@
New format (v0.6.3 or newer)
Old format (v0.6.2 or older)
Which format does the andOTP backup file have?
+ Is this TOTP Authenticator backup encrypted with a password?
Select the application you\'d like to import from
Select your desired theme
Select your desired view mode
@@ -175,7 +179,7 @@
Selected
Dark theme
Light theme
- Amoled theme
+ AMOLED theme
Normal
Compact
Small
diff --git a/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java b/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java
index 2483aa9e..12a9afd3 100644
--- a/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java
+++ b/app/src/test/java/com/beemdevelopment/aegis/SCryptTest.java
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
+import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
-import com.beemdevelopment.aegis.encoding.HexException;
import org.junit.jupiter.api.Test;
@@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.*;
public class SCryptTest {
@Test
- public void testTrailingNullCollision() throws HexException {
+ public void testTrailingNullCollision() throws EncodingException {
byte[] salt = new byte[0];
SCryptParameters params = new SCryptParameters(
CryptoUtils.CRYPTO_SCRYPT_N,
diff --git a/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java b/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java
index 58bad037..3a9653da 100644
--- a/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java
+++ b/app/src/test/java/com/beemdevelopment/aegis/TOTPTest.java
@@ -2,14 +2,14 @@ package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
-import com.beemdevelopment.aegis.encoding.HexException;
import org.junit.jupiter.api.Test;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
public class TOTPTest {
private static class Vector {
@@ -66,7 +66,7 @@ public class TOTPTest {
};
@Test
- public void vectorsMatch() throws NoSuchAlgorithmException, InvalidKeyException, HexException {
+ public void vectorsMatch() throws NoSuchAlgorithmException, InvalidKeyException {
for (Vector vector : _vectors) {
byte[] seed;