DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/VibrationPatterns.java
================================================
package com.beemdevelopment.aegis;
import java.util.Arrays;
public class VibrationPatterns {
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
public static final long[] REFRESH_CODE = {0, 100};
public static long getLengthInMillis(long[] pattern) {
return Arrays.stream(pattern).sum();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ViewMode.java
================================================
package com.beemdevelopment.aegis;
import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL,
TILES;
private static ViewMode[] _values;
static {
_values = values();
}
public static ViewMode fromInteger(int x) {
return _values[x];
}
@LayoutRes
public int getLayoutId() {
switch (this) {
case NORMAL:
return R.layout.card_entry;
case COMPACT:
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the offset (in dp) that should exist between entries in this view mode.
*/
public float getItemOffset() {
if (this == ViewMode.COMPACT) {
return 1;
} else if (this == ViewMode.TILES) {
return 4;
}
return 8;
}
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/CryptParameters.java
================================================
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
public class CryptParameters implements Serializable {
private byte[] _nonce;
private byte[] _tag;
public CryptParameters(byte[] nonce, byte[] tag) {
_nonce = nonce;
_tag = tag;
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("nonce", Hex.encode(_nonce));
obj.put("tag", Hex.encode(_tag));
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
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);
}
public byte[] getNonce() {
return _nonce;
}
public byte[] getTag() {
return _tag;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/CryptResult.java
================================================
package com.beemdevelopment.aegis.crypto;
public class CryptResult {
private byte[] _data;
private CryptParameters _params;
public CryptResult(byte[] data, CryptParameters params) {
_data = data;
_params = params;
}
public byte[] getData() {
return _data;
}
public CryptParameters getParams() {
return _params;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/CryptoUtils.java
================================================
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
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;
public static final int CRYPTO_SCRYPT_p = 1;
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
byte[] bytes = toBytes(password);
return deriveKey(bytes, params);
}
public static Cipher createEncryptCipher(SecretKey key)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
return createCipher(key, Cipher.ENCRYPT_MODE, null);
}
public static Cipher createDecryptCipher(SecretKey key, byte[] nonce)
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchPaddingException {
return createCipher(key, Cipher.DECRYPT_MODE, nonce);
}
private static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CRYPTO_AEAD);
// generate the nonce if none is given
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
if (nonce != null) {
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
cipher.init(opmode, key, spec);
} else {
cipher.init(opmode, key);
}
return cipher;
}
public static CryptResult encrypt(byte[] data, Cipher cipher)
throws BadPaddingException, IllegalBlockSizeException {
// split off the tag to store it separately
byte[] result = cipher.doFinal(data);
byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_AEAD_TAG_SIZE, result.length);
byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_AEAD_TAG_SIZE);
return new CryptResult(encrypted, new CryptParameters(cipher.getIV(), tag));
}
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, encryptedOffset, encryptedLen);
stream.write(params.getTag());
encrypted = stream.toByteArray();
byte[] decrypted = cipher.doFinal(encrypted);
return new CryptResult(decrypted, params);
}
public static SecretKey generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(CRYPTO_AEAD_KEY_SIZE * 8);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static byte[] generateSalt() {
return generateRandomBytes(CRYPTO_AEAD_KEY_SIZE);
}
public static byte[] generateRandomBytes(int length) {
SecureRandom random = new SecureRandom();
byte[] data = new byte[length];
random.nextBytes(data);
return data;
}
public static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);
return bytes;
}
@Deprecated
public static byte[] toBytesOld(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/KeyStoreHandle.java
================================================
package com.beemdevelopment.aegis.crypto;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.ProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String STORE_NAME = "AndroidKeyStore";
public KeyStoreHandle() throws KeyStoreHandleException {
try {
_keyStore = KeyStore.getInstance(STORE_NAME);
_keyStore.load(null);
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new KeyStoreHandleException(e);
}
}
public boolean containsKey(String id) throws KeyStoreHandleException {
try {
return _keyStore.containsAlias(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public SecretKey generateKey(String id) throws KeyStoreHandleException {
try {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
generator.init(new KeyGenParameterSpec.Builder(id,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(true)
.setKeySize(CryptoUtils.CRYPTO_AEAD_KEY_SIZE * 8)
.build());
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new KeyStoreHandleException(e);
}
}
public SecretKey getKey(String id) throws KeyStoreHandleException {
SecretKey key;
try {
key = (SecretKey) _keyStore.getKey(id, null);
} catch (UnrecoverableKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
if (isKeyPermanentlyInvalidated(key)) {
return null;
}
return key;
}
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
return false;
}
public void deleteKey(String id) throws KeyStoreHandleException {
try {
_keyStore.deleteEntry(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public void clear() throws KeyStoreHandleException {
try {
for (String alias : Collections.list(_keyStore.aliases())) {
deleteKey(alias);
}
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/KeyStoreHandleException.java
================================================
package com.beemdevelopment.aegis.crypto;
public class KeyStoreHandleException extends Exception {
public KeyStoreHandleException(Throwable cause) {
super(cause);
}
public KeyStoreHandleException(String message) {
super(message);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/MasterKey.java
================================================
package com.beemdevelopment.aegis.crypto;
import java.io.IOException;
import java.io.Serializable;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class MasterKey implements Serializable {
private SecretKey _key;
public MasterKey(SecretKey key) {
if (key == null) {
throw new IllegalArgumentException("Key cannot be null");
}
_key = key;
}
public static MasterKey generate() {
return new MasterKey(CryptoUtils.generateKey());
}
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
try {
Cipher cipher = CryptoUtils.createEncryptCipher(_key);
return CryptoUtils.encrypt(bytes, cipher);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new MasterKeyException(e);
}
}
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
try {
Cipher cipher = CryptoUtils.createDecryptCipher(_key, params.getNonce());
return CryptoUtils.decrypt(bytes, cipher, params);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IOException
| IllegalBlockSizeException e) {
throw new MasterKeyException(e);
}
}
public byte[] getBytes() {
return _key.getEncoded();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/MasterKeyException.java
================================================
package com.beemdevelopment.aegis.crypto;
public class MasterKeyException extends Exception {
public MasterKeyException(Throwable cause) {
super(cause);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/SCryptParameters.java
================================================
package com.beemdevelopment.aegis.crypto;
import java.io.Serializable;
public class SCryptParameters implements Serializable {
private int _n;
private int _r;
private int _p;
private byte[] _salt;
public SCryptParameters(int n, int r, int p, byte[] salt) {
_n = n;
_r = r;
_p = p;
_salt = salt;
}
public byte[] getSalt() {
return _salt;
}
public int getN() {
return _n;
}
public int getR() {
return _r;
}
public int getP() {
return _p;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/bc/SCrypt.java
================================================
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Integers;
import org.bouncycastle.util.Pack;
/**
* Implementation of the scrypt a password-based key derivation function.
*
* Scrypt was created by Colin Percival and is specified in RFC 7914 - The scrypt Password-Based Key Derivation Function
*/
public class SCrypt
{
private SCrypt()
{
// not used.
}
/**
* Generate a key using the scrypt key derivation function.
*
* @param P the bytes of the pass phrase.
* @param S the salt to use for this invocation.
* @param N CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than
* 2^(128 * r / 8).
* @param r the block size, must be >= 1.
* @param p Parallelization parameter. Must be a positive integer less than or equal to
* Integer.MAX_VALUE / (128 * r * 8).
* @param dkLen the length of the key to generate.
* @return the generated key.
*/
public static byte[] generate(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
if (P == null)
{
throw new IllegalArgumentException("Passphrase P must be provided.");
}
if (S == null)
{
throw new IllegalArgumentException("Salt S must be provided.");
}
if (N <= 1 || !isPowerOf2(N))
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and a power of 2");
}
// Only value of r that cost (as an int) could be exceeded for is 1
if (r == 1 && N >= 65536)
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and < 65536.");
}
if (r < 1)
{
throw new IllegalArgumentException("Block size r must be >= 1.");
}
int maxParallel = Integer.MAX_VALUE / (128 * r * 8);
if (p < 1 || p > maxParallel)
{
throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
+ " (based on block size r of " + r + ")");
}
if (dkLen < 1)
{
throw new IllegalArgumentException("Generated key length dkLen must be >= 1.");
}
return MFcrypt(P, S, N, r, p, dkLen);
}
private static byte[] MFcrypt(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
int MFLenBytes = r * 128;
byte[] bytes = SingleIterationPBKDF2(P, S, p * MFLenBytes);
int[] B = null;
try
{
int BLen = bytes.length >>> 2;
B = new int[BLen];
Pack.littleEndianToInt(bytes, 0, B);
/*
* Chunk memory allocations; We choose 'd' so that there will be 2**d chunks, each not
* larger than 32KiB, except that the minimum chunk size is 2 * r * 32.
*/
int d = 0, total = N * r;
while ((N - d) > 2 && total > (1 << 10))
{
++d;
total >>>= 1;
}
int MFLenWords = MFLenBytes >>> 2;
for (int BOff = 0; BOff < BLen; BOff += MFLenWords)
{
// TODO These can be done in parallel threads
SMix(B, BOff, N, d, r);
}
Pack.intToLittleEndian(B, bytes, 0);
return SingleIterationPBKDF2(P, bytes, dkLen);
}
finally
{
Clear(bytes);
Clear(B);
}
}
private static byte[] SingleIterationPBKDF2(byte[] P, byte[] S, int dkLen)
{
PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(new SHA256Digest());
pGen.init(P, S, 1);
KeyParameter key = (KeyParameter)pGen.generateDerivedMacParameters(dkLen * 8);
return key.getKey();
}
private static void SMix(int[] B, int BOff, int N, int d, int r)
{
int powN = Integers.numberOfTrailingZeros(N);
int blocksPerChunk = N >>> d;
int chunkCount = 1 << d, chunkMask = blocksPerChunk - 1, chunkPow = powN - d;
int BCount = r * 32;
int[] blockX1 = new int[16];
int[] blockX2 = new int[16];
int[] blockY = new int[BCount];
int[] X = new int[BCount];
int[][] VV = new int[chunkCount][];
try
{
System.arraycopy(B, BOff, X, 0, BCount);
for (int c = 0; c < chunkCount; ++c)
{
int[] V = new int[blocksPerChunk * BCount];
VV[c] = V;
int off = 0;
for (int i = 0; i < blocksPerChunk; i += 2)
{
System.arraycopy(X, 0, V, off, BCount);
off += BCount;
BlockMix(X, blockX1, blockX2, blockY, r);
System.arraycopy(blockY, 0, V, off, BCount);
off += BCount;
BlockMix(blockY, blockX1, blockX2, X, r);
}
}
int mask = N - 1;
for (int i = 0; i < N; ++i)
{
int j = X[BCount - 16] & mask;
int[] V = VV[j >>> chunkPow];
int VOff = (j & chunkMask) * BCount;
System.arraycopy(V, VOff, blockY, 0, BCount);
Xor(blockY, X, 0, blockY);
BlockMix(blockY, blockX1, blockX2, X, r);
}
System.arraycopy(X, 0, B, BOff, BCount);
}
finally
{
ClearAll(VV);
ClearAll(new int[][]{X, blockX1, blockX2, blockY});
}
}
private static void BlockMix(int[] B, int[] X1, int[] X2, int[] Y, int r)
{
System.arraycopy(B, B.length - 16, X1, 0, 16);
int BOff = 0, YOff = 0, halfLen = B.length >>> 1;
for (int i = 2 * r; i > 0; --i)
{
Xor(X1, B, BOff, X2);
Salsa20Engine.salsaCore(8, X2, X1);
System.arraycopy(X1, 0, Y, YOff, 16);
YOff = halfLen + BOff - YOff;
BOff += 16;
}
}
private static void Xor(int[] a, int[] b, int bOff, int[] output)
{
for (int i = output.length - 1; i >= 0; --i)
{
output[i] = a[i] ^ b[bOff + i];
}
}
private static void Clear(byte[] array)
{
if (array != null)
{
Arrays.fill(array, (byte)0);
}
}
private static void Clear(int[] array)
{
if (array != null)
{
Arrays.fill(array, 0);
}
}
private static void ClearAll(int[][] arrays)
{
for (int i = 0; i < arrays.length; ++i)
{
Clear(arrays[i]);
}
}
// note: we know X is non-zero
private static boolean isPowerOf2(int x)
{
return ((x & (x - 1)) == 0);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/bc/Salsa20Engine.java
================================================
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
/**
* Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
*/
public class Salsa20Engine {
private Salsa20Engine()
{
}
public static void salsaCore(int rounds, int[] input, int[] x)
{
if (input.length != 16)
{
throw new IllegalArgumentException();
}
if (x.length != 16)
{
throw new IllegalArgumentException();
}
if (rounds % 2 != 0)
{
throw new IllegalArgumentException("Number of rounds must be even");
}
int x00 = input[ 0];
int x01 = input[ 1];
int x02 = input[ 2];
int x03 = input[ 3];
int x04 = input[ 4];
int x05 = input[ 5];
int x06 = input[ 6];
int x07 = input[ 7];
int x08 = input[ 8];
int x09 = input[ 9];
int x10 = input[10];
int x11 = input[11];
int x12 = input[12];
int x13 = input[13];
int x14 = input[14];
int x15 = input[15];
for (int i = rounds; i > 0; i -= 2)
{
x04 ^= Integer.rotateLeft(x00 + x12, 7);
x08 ^= Integer.rotateLeft(x04 + x00, 9);
x12 ^= Integer.rotateLeft(x08 + x04, 13);
x00 ^= Integer.rotateLeft(x12 + x08, 18);
x09 ^= Integer.rotateLeft(x05 + x01, 7);
x13 ^= Integer.rotateLeft(x09 + x05, 9);
x01 ^= Integer.rotateLeft(x13 + x09, 13);
x05 ^= Integer.rotateLeft(x01 + x13, 18);
x14 ^= Integer.rotateLeft(x10 + x06, 7);
x02 ^= Integer.rotateLeft(x14 + x10, 9);
x06 ^= Integer.rotateLeft(x02 + x14, 13);
x10 ^= Integer.rotateLeft(x06 + x02, 18);
x03 ^= Integer.rotateLeft(x15 + x11, 7);
x07 ^= Integer.rotateLeft(x03 + x15, 9);
x11 ^= Integer.rotateLeft(x07 + x03, 13);
x15 ^= Integer.rotateLeft(x11 + x07, 18);
x01 ^= Integer.rotateLeft(x00 + x03, 7);
x02 ^= Integer.rotateLeft(x01 + x00, 9);
x03 ^= Integer.rotateLeft(x02 + x01, 13);
x00 ^= Integer.rotateLeft(x03 + x02, 18);
x06 ^= Integer.rotateLeft(x05 + x04, 7);
x07 ^= Integer.rotateLeft(x06 + x05, 9);
x04 ^= Integer.rotateLeft(x07 + x06, 13);
x05 ^= Integer.rotateLeft(x04 + x07, 18);
x11 ^= Integer.rotateLeft(x10 + x09, 7);
x08 ^= Integer.rotateLeft(x11 + x10, 9);
x09 ^= Integer.rotateLeft(x08 + x11, 13);
x10 ^= Integer.rotateLeft(x09 + x08, 18);
x12 ^= Integer.rotateLeft(x15 + x14, 7);
x13 ^= Integer.rotateLeft(x12 + x15, 9);
x14 ^= Integer.rotateLeft(x13 + x12, 13);
x15 ^= Integer.rotateLeft(x14 + x13, 18);
}
x[ 0] = x00 + input[ 0];
x[ 1] = x01 + input[ 1];
x[ 2] = x02 + input[ 2];
x[ 3] = x03 + input[ 3];
x[ 4] = x04 + input[ 4];
x[ 5] = x05 + input[ 5];
x[ 6] = x06 + input[ 6];
x[ 7] = x07 + input[ 7];
x[ 8] = x08 + input[ 8];
x[ 9] = x09 + input[ 9];
x[10] = x10 + input[10];
x[11] = x11 + input[11];
x[12] = x12 + input[12];
x[13] = x13 + input[13];
x[14] = x14 + input[14];
x[15] = x15 + input[15];
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/otp/HOTP.java
================================================
package com.beemdevelopment.aegis.crypto.otp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class HOTP {
private HOTP() {
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] hash = getHash(secret, algo, counter);
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
return new OTP(otp, digits);
}
public static byte[] getHash(byte[] secret, String algo, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
return mac.doFinal(counterBytes);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/otp/MOTP.java
================================================
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.beemdevelopment.aegis.encoding.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MOTP {
private final String _code;
private final int _digits;
private MOTP(String code, int digits) {
_code = code;
_digits = digits;
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
throws NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
throws NoSuchAlgorithmException {
long timeBasedCounter = time / period;
String secretAsString = Hex.encode(secret);
String toDigest = timeBasedCounter + secretAsString + pin;
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
return new MOTP(code, digits);
}
@VisibleForTesting
@NonNull
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algo);
byte[] digest = md.digest(toDigest);
return Hex.encode(digest);
}
@NonNull
@Override
public String toString() {
return _code.substring(0, _digits);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java
================================================
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private final int _code;
private final int _digits;
public OTP(int code, int digits) {
_code = code;
_digits = digits;
}
public int getCode() {
return _code;
}
public int getDigits() {
return _digits;
}
@NonNull
@Override
public String toString() {
int code = _code % (int) Math.pow(10, _digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(code));
while (res.length() < _digits) {
res.insert(0, "0");
}
return res.toString();
}
public String toSteamString() {
int code = _code;
StringBuilder res = new StringBuilder();
for (int i = 0; i < _digits; i++) {
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
res.append(c);
code /= STEAM_ALPHABET.length();
}
return res.toString();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/otp/TOTP.java
================================================
package com.beemdevelopment.aegis.crypto.otp;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TOTP {
private TOTP() {
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/otp/YAOTP.java
================================================
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class YAOTP {
private static final int EN_ALPHABET_LENGTH = 26;
private final long _code;
private final int _digits;
private YAOTP(long code, int digits) {
_code = code;
_digits = digits;
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
long seconds = System.currentTimeMillis() / 1000;
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
byte[] pinWithHash;
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
stream.write(pinBytes);
stream.write(secret);
pinWithHash = stream.toByteArray();
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(pinWithHash);
if (keyHash[0] == 0) {
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
}
long counter = (long) Math.floor((double) seconds / period);
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
int offset = periodHash[periodHash.length - 1] & 0xf;
periodHash[offset] &= 0x7f;
long otp = ByteBuffer.wrap(periodHash)
.order(ByteOrder.BIG_ENDIAN)
.getLong(offset);
return new YAOTP(otp, digits);
}
@NonNull
@Override
public String toString() {
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
char[] chars = new char[_digits];
for (int i = _digits - 1; i >= 0; i--) {
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
code /= EN_ALPHABET_LENGTH;
}
return new String(chars);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/crypto/pins/GuardianProjectFDroidRSA2048.java
================================================
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
public GuardianProjectFDroidRSA2048() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/database/AppDatabase.java
================================================
package com.beemdevelopment.aegis.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {AuditLogEntry.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AuditLogDao auditLogDao();
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/database/AuditLogDao.java
================================================
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface AuditLogDao {
@Insert
void insert(AuditLogEntry log);
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
LiveData> getAll();
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/database/AuditLogEntry.java
================================================
package com.beemdevelopment.aegis.database;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.beemdevelopment.aegis.EventType;
@Entity(tableName = "audit_logs")
public class AuditLogEntry {
@PrimaryKey(autoGenerate = true)
protected long id;
@NonNull
@ColumnInfo(name = "event_type")
private final EventType _eventType;
@ColumnInfo(name = "reference")
private final String _reference;
@ColumnInfo(name = "timestamp")
private final long _timestamp;
@Ignore
public AuditLogEntry(@NonNull EventType eventType) {
this(eventType, null);
}
@Ignore
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
_eventType = eventType;
_reference = reference;
_timestamp = System.currentTimeMillis();
}
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
this.id = id;
_eventType = eventType;
_reference = reference;
_timestamp = timestamp;
}
public long getId() {
return id;
}
public EventType getEventType() {
return _eventType;
}
public String getReference() {
return _reference;
}
public long getTimestamp() {
return _timestamp;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/database/AuditLogRepository.java
================================================
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import com.beemdevelopment.aegis.EventType;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AuditLogRepository {
private final AuditLogDao _auditLogDao;
private final Executor _executor;
public AuditLogRepository(AuditLogDao auditLogDao) {
_auditLogDao = auditLogDao;
_executor = Executors.newSingleThreadExecutor();
}
public LiveData> getAllAuditLogEntries() {
return _auditLogDao.getAll();
}
public void addVaultUnlockedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
insert(auditLogEntry);
}
public void addBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addAndroidBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addVaultExportedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
insert(auditLogEntry);
}
public void addEntrySharedEvent(String reference) {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
insert(auditLogEntry);
}
public void addVaultUnlockFailedPasswordEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
insert(auditLogEntry);
}
public void addVaultUnlockFailedBiometricsEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
insert(auditLogEntry);
}
public void insert(AuditLogEntry auditLogEntry) {
_executor.execute(() -> {
_auditLogDao.insert(auditLogEntry);
});
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java
================================================
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public class Base32 {
private Base32() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
public static String encode(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return encode(bytes);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/encoding/Base64.java
================================================
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class Base64 {
private Base64() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException 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) {
return BaseEncoding.base64().encode(data);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java
================================================
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
public EncodingException(String message) {
super(message);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/encoding/Hex.java
================================================
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
public class Hex {
private Hex() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return BaseEncoding.base16().lowerCase().encode(data);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/AnimationsHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.provider.Settings;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
public class AnimationsHelper {
private AnimationsHelper() {
}
public static Animation loadScaledAnimation(Context context, int animationResId) {
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
}
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
animation.setDuration(newDuration);
return animation;
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
Animation animation = controller.getAnimation();
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
return controller;
}
public enum Scale {
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
private final String _setting;
Scale(String setting) {
_setting = setting;
}
public float getValue(Context context) {
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
}
public boolean isZero(Context context) {
return getValue(context) == 0;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricSlotInitializer.java
================================================
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/BiometricsHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricPrompt.ERROR_CANCELED
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/BitmapHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import java.io.ByteArrayOutputStream;
import java.util.Objects;
public class BitmapHelper {
private BitmapHelper() {
}
/**
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
*/
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
if (maxHeight <= 0 || maxWidth <= 0) {
return bitmap;
}
float maxRatio = (float) maxWidth / maxHeight;
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
int width = maxWidth;
int height = maxHeight;
if (maxRatio > 1) {
width = (int) ((float) maxHeight * ratio);
} else {
height = (int) ((float) maxWidth / ratio);
}
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
}
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Objects.equals(iconType, IconType.PNG)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
} else {
iconType = IconType.JPEG;
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}
byte[] data = stream.toByteArray();
return new VaultEntryIcon(data, iconType);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/CenterVerticalSpan.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
public class CenterVerticalSpan extends MetricAffectingSpan {
Rect _substringBounds;
public CenterVerticalSpan(Rect substringBounds) {
_substringBounds = substringBounds;
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
@Override
public void updateDrawState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
private void applyBaselineShift(TextPaint textPaint) {
float topDifference = textPaint.getFontMetrics().top - _substringBounds.top;
textPaint.baselineShift -= (topDifference / 2f);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/ContextHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.ContextWrapper;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import javax.annotation.Nullable;
/**
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
*/
public class ContextHelper {
private ContextHelper() {
}
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
@Nullable
public static ComponentActivity getActivity(@NonNull Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof ComponentActivity) {
return (ComponentActivity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
@Nullable
public static Lifecycle getLifecycle(@NonNull Context context) {
ComponentActivity activity = getActivity(context);
return activity == null ? null : activity.getLifecycle();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/DropdownHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.ArrayRes;
import com.beemdevelopment.aegis.R;
import java.util.List;
public class DropdownHelper {
private DropdownHelper() {
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
ArrayAdapter adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
dropdown.setAdapter(adapter);
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, List items) {
ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
dropdown.setAdapter(adapter);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/EditTextHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.widget.EditText;
import java.util.Arrays;
public class EditTextHelper {
private EditTextHelper() {
}
public static char[] getEditTextChars(EditText text) {
Editable editable = text.getText();
char[] chars = new char[editable.length()];
editable.getChars(0, editable.length(), chars, 0);
return chars;
}
public static boolean areEditTextsEqual(EditText text1, EditText text2) {
char[] password = getEditTextChars(text1);
char[] passwordConfirm = getEditTextChars(text2);
return password.length != 0 && Arrays.equals(password, passwordConfirm);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/FabMenuHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.animation.ValueAnimator;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.OvershootInterpolator;
import android.widget.ImageView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class FabMenuHelper {
private final static long ANIMATION_DURATION = 300L;
private final static long ANIMATION_ACTION_DELAY = 50L;
private final View _scrim;
private final View _menuItemsContainer;
private final FloatingActionButton _mainFab;
private final List _actions;
private Consumer _stateListener;
private boolean _isOpen = false;
public FabMenuHelper(
View scrim,
ViewGroup menuItemsContainer,
FloatingActionButton fab,
Map actions
) {
_scrim = scrim;
_menuItemsContainer = menuItemsContainer;
_mainFab = fab;
_actions = new ArrayList<>(actions.keySet());
for (View action : _actions) {
action.setVisibility(View.GONE);
action.setAlpha(0f);
action.setScaleX(0f);
action.setScaleY(0f);
}
setupClickListeners(actions);
}
public void setOnFabMenuStateChangeListener(Consumer listener) {
_stateListener = listener;
}
private void setupClickListeners(Map actions) {
_mainFab.setOnClickListener(v -> toggle());
_scrim.setOnClickListener(v -> close());
actions.forEach((action, onClick) -> {
action.setOnClickListener(v -> {
if (onClick != null) {
onClick.run();
}
close();
});
});
}
public void toggle() {
if (_isOpen) {
close();
} else {
open();
}
}
public void open() {
if (_isOpen) {
return;
}
_isOpen = true;
_scrim.animate()
.alpha(0.5f)
.setDuration(ANIMATION_DURATION)
.withStartAction(() -> _scrim.setVisibility(View.VISIBLE))
.start();
_menuItemsContainer.setVisibility(View.VISIBLE);
long delay = 0L;
for (int i = _actions.size() - 1; i >= 0; i--) {
animateActionIn(_actions.get(i), delay);
delay += ANIMATION_ACTION_DELAY;
}
animateFabIconForward(_mainFab);
if (_stateListener != null) {
_stateListener.accept(true);
}
}
public void close() {
if (!_isOpen) {
return;
}
_isOpen = false;
_scrim.animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION)
.withEndAction(() -> _scrim.setVisibility(View.GONE))
.start();
long delay = 0L;
for (View action : _actions) {
animateActionOut(action, delay);
delay += ANIMATION_ACTION_DELAY;
}
animateFabIconBackward(_mainFab);
_mainFab.postDelayed(() -> {
if (!_isOpen) {
_menuItemsContainer.setVisibility(View.GONE);
}
}, ANIMATION_DURATION);
if (_stateListener != null) {
_stateListener.accept(false);
}
}
private void animateFabIconForward(FloatingActionButton fab) {
animateFabIcon(fab, 0f, 45f);
}
private void animateFabIconBackward(FloatingActionButton fab) {
animateFabIcon(fab, 45f, 0f);
}
private void animateFabIcon(FloatingActionButton fab, float from, float to) {
Drawable drawable = _mainFab.getDrawable();
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
fab.setScaleType(ImageView.ScaleType.MATRIX);
Matrix matrix = new Matrix();
ValueAnimator anim = ValueAnimator.ofFloat(from, to);
anim.setDuration(100L);
anim.addUpdateListener(valueAnimator -> {
Float angle = (Float) valueAnimator.getAnimatedValue();
matrix.reset();
matrix.postRotate(angle, width / 2f, height / 2f);
fab.setImageMatrix(matrix);
});
anim.start();
}
private void animateActionIn(View action, long delay) {
action.setVisibility(View.VISIBLE);
action.setAlpha(0f);
action.setScaleX(0.4f);
action.setScaleY(0.4f);
action.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(ANIMATION_DURATION)
.setStartDelay(delay)
.setInterpolator(new OvershootInterpolator(1.2f))
.start();
}
private void animateActionOut(View action, long delay) {
action.animate()
.alpha(0f)
.scaleX(0f)
.scaleY(0f)
.setDuration(ANIMATION_DURATION)
.setStartDelay(delay)
.withEndAction(() -> action.setVisibility(View.GONE))
.start();
}
public boolean isOpen() {
return _isOpen;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/FabScrollHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 2 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
setVisible(false);
} else if (dy < -2 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
setVisible(true);
}
}
public void setVisible(boolean visible) {
if (visible) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
} else {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/ItemTouchHelperAdapter.java
================================================
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
public interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and not at the end of a "drop" event.
*
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemMove(int fromPosition, int toPosition);
/**
* Called when an item has been dismissed by a swipe.
*
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemDismiss(int position);
/**
* Called when an item has been dropped after a drag.
*
* @param position The position of the moved item.
*/
void onItemDrop(int position);
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/MetricsHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.util.DisplayMetrics;
public class MetricsHelper {
private MetricsHelper() {
}
public static int convertDpToPixels(Context context, float dp) {
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/PasswordStrengthHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
public class PasswordStrengthHelper {
// Limit the password length to prevent zxcvbn4j from exploding
private static final int MAX_PASSWORD_LENGTH = 64;
// Material design color palette
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
private final Zxcvbn _zxcvbn = new Zxcvbn();
private final EditText _textPassword;
private final ProgressBar _barPasswordStrength;
private final TextView _textPasswordStrength;
private final TextInputLayout _textPasswordWrapper;
public PasswordStrengthHelper(
EditText textPassword,
ProgressBar barPasswordStrength,
TextView textPasswordStrength,
TextInputLayout textPasswordWrapper
) {
_textPassword = textPassword;
_barPasswordStrength = barPasswordStrength;
_textPasswordStrength = textPasswordStrength;
_textPasswordWrapper = textPasswordWrapper;
}
public void measure(Context context) {
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
_barPasswordStrength.setProgress(0);
_textPasswordStrength.setText(R.string.password_strength_unknown);
} else {
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
String warning = strength.getFeedback().getWarning();
_textPasswordWrapper.setError(warning);
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
strength.wipe();
}
}
private static String getString(int score, Context context) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
String[] strings = context.getResources().getStringArray(R.array.password_strength);
return strings[score];
}
private static String getColor(int score) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
return COLORS[score];
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/PermissionHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionHelper {
private PermissionHelper() {
}
public static boolean granted(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
public static boolean request(Activity activity, int requestCode, String... perms) {
List deniedPerms = new ArrayList<>();
for (String permission : perms) {
if (!granted(activity, permission)) {
deniedPerms.add(permission);
}
}
int size = deniedPerms.size();
if (size > 0) {
String[] array = new String[size];
ActivityCompat.requestPermissions(activity, deniedPerms.toArray(array), requestCode);
}
return size == 0;
}
public static boolean checkResults(int[] grantResults) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeAnalyzer.java
================================================
package com.beemdevelopment.aegis.helpers;
import static android.graphics.ImageFormat.YUV_420_888;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import java.nio.ByteBuffer;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
public static final Size RESOLUTION = new Size(1200, 1600);
private final QrCodeAnalyzer.Listener _listener;
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
_listener = listener;
}
@Override
public void analyze(@NonNull ImageProxy image) {
int format = image.getFormat();
if (format != YUV_420_888) {
Log.e(TAG, String.format("Unexpected YUV image format: %d", format));
image.close();
return;
}
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
ByteBuffer buf = plane.getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data,
plane.getRowStride(),
image.getHeight(),
0,
0,
image.getWidth(),
image.getHeight(),
false
);
try {
Result result = QrCodeHelper.decodeFromSource(source);
if (_listener != null) {
_listener.onQrCodeDetected(result);
}
} catch (NotFoundException ignored) {
} finally {
image.close();
}
}
public interface Listener {
void onQrCodeDetected(Result result);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/QrCodeHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class QrCodeHelper {
private QrCodeHelper() {
}
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
Map hints = new HashMap<>();
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
hints.put(DecodeHintType.ALSO_INVERTED, true);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
MultiFormatReader reader = new MultiFormatReader();
return reader.decode(bitmap, hints);
}
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
if (bitmap == null) {
throw new DecodeError("Unable to decode stream to bitmap");
}
// If ZXing is not able to decode the image on the first try, we try a couple of
// more times with smaller versions of the same image.
for (int i = 0; i <= 2; i++) {
if (i != 0) {
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
}
try {
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
return decodeFromSource(source);
} catch (NotFoundException ignored) {
}
}
throw new DecodeError(NotFoundException.getNotFoundInstance());
}
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
public static class DecodeError extends Exception {
public DecodeError(String message) {
super(message);
}
public DecodeError(Throwable cause) {
super(cause);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/SafHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.webkit.MimeTypeMap;
import androidx.documentfile.provider.DocumentFile;
public class SafHelper {
private SafHelper() {
}
public static String getFileName(Context context, Uri uri) {
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (i != -1) {
return cursor.getString(i);
}
}
}
}
return uri.getLastPathSegment();
}
public static String getMimeType(Context context, Uri uri) {
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
if (file != null) {
String fileType = file.getType();
if (fileType != null) {
return fileType;
}
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (ext != null) {
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
}
return null;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleAnimationEndListener.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.view.animation.Animation;
public class SimpleAnimationEndListener implements Animation.AnimationListener {
private final Listener _listener;
public SimpleAnimationEndListener(Listener listener) {
_listener = listener;
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (_listener != null) {
_listener.onAnimationEnd(animation);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
public interface Listener {
void onAnimationEnd(Animation animation);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleItemTouchHelperCallback.java
================================================
package com.beemdevelopment.aegis.helpers;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return _isLongPressDragEnabled;
}
public void setIsLongPressDragEnabled(boolean enabled) {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(VaultEntry entry) {
if (entry == null) {
_selectedEntry = null;
return;
}
if (!entry.isFavorite()) {
_selectedEntry = entry;
}
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
// that's passed to this function has a position of -1, leading
// to a crash down the line.
int position = viewHolder.getBindingAdapterPosition();
if (position == NO_POSITION) {
return 0;
}
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter == null) {
return 0;
}
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPosition(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
if (targetIndex < _adapter.getShownFavoritesCount()) {
return false;
}
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getBindingAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/SimpleTextWatcher.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.text.TextWatcher;
public final class SimpleTextWatcher implements TextWatcher {
private final Listener _listener;
public SimpleTextWatcher(Listener listener) {
_listener = listener;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (_listener != null) {
_listener.afterTextChanged(s);
}
}
public interface Listener {
void afterTextChanged(Editable s);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/TextDrawableHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.view.View;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.text.BreakIterator;
import java.util.Arrays;
public class TextDrawableHelper {
// taken from: https://materialuicolors.co (level 700)
private static ColorGenerator _generator = ColorGenerator.create(Arrays.asList(
0xFFD32F2F,
0xFFC2185B,
0xFF7B1FA2,
0xFF512DA8,
0xFF303F9F,
0xFF1976D2,
0xFF0288D1,
0xFF0097A7,
0xFF00796B,
0xFF388E3C,
0xFF689F38,
0xFFAFB42B,
0xFFFBC02D,
0xFFFFA000,
0xFFF57C00,
0xFFE64A19,
0xFF5D4037,
0xFF616161,
0xFF455A64
));
private TextDrawableHelper() {
}
public static TextDrawable generate(String text, String fallback, View view) {
if (text == null || text.isEmpty()) {
if (fallback == null || fallback.isEmpty()) {
return null;
}
text = fallback;
}
int color = _generator.getColor(text);
return TextDrawable.builder().beginConfig()
.width(view.getLayoutParams().width)
.height(view.getLayoutParams().height)
.endConfig()
.buildRound(getFirstGrapheme(text).toUpperCase(), color);
}
private static String getFirstGrapheme(String text) {
BreakIterator iter = BreakIterator.getCharacterInstance();
iter.setText(text);
int start = iter.first(), end = iter.next();
if (end == BreakIterator.DONE) {
return "";
}
return text.substring(start, end);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/ThemeHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.res.Configuration;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.color.DynamicColorsOptions;
import java.util.Map;
public class ThemeHelper {
private final AppCompatActivity _activity;
private final Preferences _prefs;
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
_activity = activity;
_prefs = prefs;
}
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
public void setTheme(Map themeMap) {
int theme = themeMap.get(getConfiguredTheme());
_activity.setTheme(theme);
if (_prefs.isDynamicColorsEnabled()) {
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
if (getConfiguredTheme().equals(Theme.AMOLED)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
} else if (getConfiguredTheme().equals(Theme.DARK)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
}
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
}
}
public Theme getConfiguredTheme() {
Theme theme = _prefs.getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/UiRefresher.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import com.beemdevelopment.aegis.VibrationPatterns;
public class UiRefresher {
private boolean _running;
private Listener _listener;
private Handler _handler;
public UiRefresher(Listener listener) {
_listener = listener;
_handler = new Handler();
}
public void destroy() {
stop();
_listener = null;
}
public void start() {
if (_running) {
return;
}
_running = true;
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onExpiring();
_handler.postDelayed(this, getNextRun());
}
}, getInitialRun());
}
private long getInitialRun() {
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
if (sum < 0) {
return getNextRun();
}
return sum;
}
private long getNextRun() {
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
}
public void stop() {
_handler.removeCallbacksAndMessages(null);
_running = false;
}
public interface Listener {
void onRefresh();
void onExpiring();
long getMillisTillNextRefresh();
long getPeriodMillis();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/UiThreadExecutor.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/VibrationHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import com.beemdevelopment.aegis.Preferences;
public class VibrationHelper {
private Preferences _preferences;
public VibrationHelper(Context context) {
_preferences = new Preferences(context);
}
public void vibratePattern(Context context, long[] pattern) {
if (!isHapticFeedbackEnabled()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
if (vibratorManager != null) {
Vibrator vibrator = vibratorManager.getDefaultVibrator();
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
} else {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
}
}
}
public boolean isHapticFeedbackEnabled() {
return _preferences.isHapticFeedbackEnabled();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/ViewHelper.java
================================================
package com.beemdevelopment.aegis.helpers;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.appbar.AppBarLayout;
public class ViewHelper {
private ViewHelper() {
}
public static void setupAppBarInsets(AppBarLayout appBar) {
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
insets.left,
insets.top,
insets.right,
0
);
return WindowInsetsCompat.CONSUMED;
});
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/comparators/AccountNameComparator.java
================================================
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/comparators/FavoriteComparator.java
================================================
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class FavoriteComparator implements Comparator {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return -1 * Boolean.compare(a.isFavorite(), b.isFavorite());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/comparators/IssuerNameComparator.java
================================================
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/comparators/LastUsedComparator.java
================================================
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class LastUsedComparator implements Comparator {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/helpers/comparators/UsageCountComparator.java
================================================
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class UsageCountComparator implements Comparator {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Integer.compare(a.getUsageCount(), b.getUsageCount());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/icons/IconPack.java
================================================
package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.base.Objects;
import com.google.common.io.Files;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class IconPack {
private UUID _uuid;
private String _name;
private int _version;
private List _icons;
private File _dir;
private IconPack(UUID uuid, String name, int version, List icons) {
_uuid = uuid;
_name = name;
_version = version;
_icons = icons;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public int getVersion() {
return _version;
}
public List getIcons() {
return Collections.unmodifiableList(_icons);
}
/**
* Retrieves a list of icons suggested for the given issuer.
*/
public List getSuggestedIcons(String issuer) {
if (issuer == null || issuer.isEmpty()) {
return new ArrayList<>();
}
List icons = new ArrayList<>();
for (Icon icon : _icons) {
MatchType matchType = icon.getMatchFor(issuer);
if (matchType != null) {
// Inverse matches (entry issuer contains icon name) are less likely
// to be good, so position them at the end of the list.
if (matchType.equals(MatchType.NORMAL)) {
icons.add(0, icon);
} else if (matchType.equals(MatchType.INVERSE)) {
icons.add(icon);
}
}
}
return icons;
}
@Nullable
public File getDirectory() {
return _dir;
}
void setDirectory(@NonNull File dir) {
_dir = dir;
}
/**
* Indicates whether some other object is "equal to" this one. The object does not
* necessarily have to be the same instance. Equality of UUID and version will make
* this method return true;
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof IconPack)) {
return false;
}
IconPack pack = (IconPack) o;
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
}
@Override
public int hashCode() {
return Objects.hashCode(_uuid, _version);
}
public static IconPack fromJson(JSONObject obj) throws JSONException {
UUID uuid;
String uuidString = obj.getString("uuid");
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
}
String name = obj.getString("name");
int version = obj.getInt("version");
JSONArray array = obj.getJSONArray("icons");
List icons = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Icon icon = Icon.fromJson(array.getJSONObject(i));
icons.add(icon);
}
return new IconPack(uuid, name, version, icons);
}
public static IconPack fromBytes(byte[] data) throws JSONException {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return IconPack.fromJson(obj);
}
public static class Icon implements Serializable {
private final String _relFilename;
private final String _name;
private final String _category;
private final List _issuers;
private File _file;
protected Icon(String filename, String name, String category, List issuers) {
_relFilename = filename;
_name = name;
_category = category;
_issuers = issuers;
}
public String getRelativeFilename() {
return _relFilename;
}
@Nullable
public File getFile() {
return _file;
}
void setFile(@NonNull File file) {
_file = file;
}
public IconType getIconType() {
return IconType.fromFilename(_relFilename);
}
public String getName() {
if (_name != null) {
return _name;
}
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
public String getCategory() {
return _category;
}
private MatchType getMatchFor(String issuer) {
String lowerEntryIssuer = issuer.toLowerCase();
boolean inverseMatch = false;
for (String is : _issuers) {
String lowerIconIssuer = is.toLowerCase();
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
return MatchType.NORMAL;
}
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
inverseMatch = true;
}
}
if (inverseMatch) {
return MatchType.INVERSE;
}
return null;
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String name = JsonUtils.optString(obj, "name");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
List issuers = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
String issuer = array.getString(i);
issuers.add(issuer);
}
return new Icon(filename, name, category, issuers);
}
}
private enum MatchType {
NORMAL,
INVERSE
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/icons/IconPackException.java
================================================
package com.beemdevelopment.aegis.icons;
public class IconPackException extends Exception {
public IconPackException(Throwable cause) {
super(cause);
}
public IconPackException(String message) {
super(message);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/icons/IconPackExistsException.java
================================================
package com.beemdevelopment.aegis.icons;
public class IconPackExistsException extends IconPackException {
private IconPack _pack;
public IconPackExistsException(IconPack pack) {
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
_pack = pack;
}
public IconPack getIconPack() {
return _pack;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/icons/IconPackManager.java
================================================
package com.beemdevelopment.aegis.icons;
import android.content.Context;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.IOUtils;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPackManager {
private static final String _packDefFilename = "pack.json";
private File _iconsBaseDir;
private List _iconPacks;
public IconPackManager(Context context) {
_iconPacks = new ArrayList<>();
_iconsBaseDir = new File(context.getFilesDir(), "icons");
rescanIconPacks();
}
private IconPack getIconPackByUUID(UUID uuid) {
List packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
if (packs.size() == 0) {
return null;
}
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List getIconPacks() {
return new ArrayList<>(_iconPacks);
}
public void removeIconPack(IconPack pack) throws IconPackException {
try {
File dir = getIconPackDir(pack);
deleteDir(dir);
} catch (IOException e) {
throw new IconPackException(e);
}
_iconPacks.remove(pack);
}
public IconPack importPack(File inFile) throws IconPackException {
try {
// read and parse the icon pack definition file of the icon pack
ZipFile zipFile = new ZipFile(inFile);
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
if (packHeader == null) {
throw new IOException("Unable to find pack.json in the root of the ZIP file");
}
IconPack pack;
byte[] defBytes;
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
defBytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(defBytes);
}
// create a new directory to store the icon pack, based on the UUID and version
File packDir = getIconPackDir(pack);
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
throw new IOException("Attempted to write outside of the parent directory");
}
if (packDir.exists()) {
throw new IconPackExistsException(pack);
}
IconPack existingPack = getIconPackByUUID(pack.getUUID());
if (existingPack != null) {
throw new IconPackExistsException(existingPack);
}
if (!packDir.exists() && !packDir.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
// extract each of the defined icons to the icon pack directory
for (IconPack.Icon icon : pack.getIcons()) {
File destFile = new File(packDir, icon.getRelativeFilename());
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
if (iconHeader == null) {
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
}
// create new directories for this file if needed
File parent = destFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
FileOutputStream outStream = new FileOutputStream(destFile)) {
IOUtils.copy(inStream, outStream);
}
// after successful copy of the icon, store the new filename
icon.setFile(destFile);
}
// write the icon pack definition file to the newly created directory
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
outStream.write(defBytes);
}
// after successful extraction of the icon pack, store the new directory
pack.setDirectory(packDir);
_iconPacks.add(pack);
return pack;
} catch (IOException | JSONException e) {
throw new IconPackException(e);
}
}
private void rescanIconPacks() {
_iconPacks.clear();
File[] dirs = _iconsBaseDir.listFiles();
if (dirs == null) {
return;
}
for (File dir : dirs) {
if (!dir.isDirectory()) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(dir.getName());
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
}
File versionDir = getLatestVersionDir(dir);
if (versionDir != null) {
IconPack pack;
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
byte[] bytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(bytes);
pack.setDirectory(versionDir);
} catch (JSONException | IOException e) {
e.printStackTrace();
continue;
}
for (IconPack.Icon icon : pack.getIcons()) {
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
}
// do a sanity check on the UUID and version
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
_iconPacks.add(pack);
}
}
}
}
private File getIconPackDir(IconPack pack) {
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
}
@Nullable
private static File getLatestVersionDir(File packDir) {
File[] dirs = packDir.listFiles();
if (dirs == null) {
return null;
}
int latestVersion = -1;
for (File versionDir : dirs) {
int version;
try {
version = Integer.parseInt(versionDir.getName());
} catch (NumberFormatException ignored) {
continue;
}
if (latestVersion == -1 || version > latestVersion) {
latestVersion = version;
}
}
if (latestVersion == -1) {
return null;
}
return new File(packDir, Integer.toString(latestVersion));
}
private static void deleteDir(File dir) throws IOException {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDir(child);
}
}
}
if (!dir.delete()) {
throw new IOException(String.format("Unable to delete directory: %s", dir));
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/icons/IconType.java
================================================
package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
import java.util.Locale;
public enum IconType {
INVALID,
SVG,
PNG,
JPEG;
public static IconType fromMimeType(String mimeType) {
switch (mimeType) {
case "image/svg+xml":
return SVG;
case "image/png":
return PNG;
case "image/jpeg":
return JPEG;
default:
return INVALID;
}
}
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase(Locale.ROOT)) {
case "svg":
return SVG;
case "png":
return PNG;
case "jpg":
// intentional fallthrough
case "jpeg":
return JPEG;
default:
return INVALID;
}
}
public String toMimeType() {
switch (this) {
case SVG:
return "image/svg+xml";
case PNG:
return "image/png";
case JPEG:
return "image/jpeg";
default:
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/AegisImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
public AegisImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (VaultFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private VaultFile _file;
private EncryptedState(VaultFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
JSONObject obj;
try {
obj = _file.getContent(creds);
} catch (VaultFileException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj, creds);
}
public State decrypt(char[] password) throws DatabaseImporterException {
List slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
return decrypt(creds);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
List slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
try {
if (result == null) {
throw new DatabaseImporterException("Password incorrect");
}
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
State state = decrypt(creds);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
}
}
public static class DecryptedState extends State {
private JSONObject _obj;
private VaultFileCredentials _creds;
private DecryptedState(JSONObject obj) {
this(obj, null);
}
private DecryptedState(JSONObject obj, VaultFileCredentials creds) {
super(false);
_obj = obj;
_creds = creds;
}
@Nullable
public VaultFileCredentials getCredentials() {
return _creds;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/AndOtpImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
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.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
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 java.util.Locale;
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 {
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = IOUtils.readAll(stream);
} 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);
}
}
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;
}
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
try {
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, 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);
}
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
// If number of iterations is this high, it's probably not an andOTP file, so
// abort early in order to prevent having to wait for an extremely long key derivation
// process, only to find out that the user picked the wrong file
if (iterations > 10_000_000L) {
throw new DatabaseImporterException(String.format("Unexpectedly high number of iterations: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
return decryptContent(key, 0);
}
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
return decryptContent(key, INT_SIZE + SALT_SIZE);
}
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decryptNewFormat(key);
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
})
.create());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _obj;
private DecryptedState(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | EncodingException | OtpInfoException |
JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/AuthenticatorPlusImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
public AuthenticatorPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
private final byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected State decrypt(char[] password) throws DatabaseImporterException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
return importer.read(zipStream);
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/AuthyImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.R;
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.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
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.io.InputStream;
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;
import javax.crypto.spec.SecretKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs";
private static final String _pkgName = "com.authy.authy";
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
private static final String _authyFilename = "com.authy.storage.tokens.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);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
JSONArray array;
JSONArray authyArray;
try {
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
file1.setShell(shell);
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
file2.setShell(shell);
array = readFile(file1, String.format("%s.key", _authFilename));
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
try {
for (int i = 0; i < authyArray.length(); i++) {
array.put(authyArray.getJSONObject(i));
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return read(array);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(String.format("%s.key", _authFilename))
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
array = new JSONArray(entry.Value);
break;
}
}
return read(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
private State read(JSONArray array) throws DatabaseImporterException {
try {
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
return new EncryptedState(array);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(array);
}
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
try (InputStream inStream = SuFileInputStream.open(file)) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(inStream, null);
parser.nextTag();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(key)) {
return new JSONArray(entry.Value);
}
}
} catch (JSONException ignored) {
}
return new JSONArray();
}
public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _array;
private EncryptedState(JSONArray array) {
super(true);
_array = array;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
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);
key = new SecretKeySpec(key.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
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));
}
return new DecryptedState(_array);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false);
_array = array;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
boolean isAuthy = entry.has("secretSeed");
sanitizeEntryInfo(authyEntryInfo, isAuthy);
byte[] secret;
if (isAuthy) {
secret = Hex.decode(entry.getString("secretSeed"));
} else {
secret = Base32.decode(entry.getString("decryptedSecret"));
}
int digits = entry.getInt("digits");
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
if (!isAuthy) {
String separator = "";
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(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + separator, "");
} else {
info.Issuer = info.Name;
info.Name = "";
}
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}
private static class AuthyEntryInfo {
String OriginalName;
String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/BattleNetImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
public class BattleNetImporter extends DatabaseImporter {
private static final String _pkgName = "com.blizzard.messenger";
private static final String _subPath = "shared_prefs/com.blizzard.messenger.authenticator_preferences.xml";
private static final byte[] _key;
public BattleNetImporter(Context context) {
super(context);
}
static {
try {
_key = Hex.decode("398e27fc50276a656065b0e525f4c06c04c61075286b8e7aeda59da9813b5dd6c80d2fb38068773fa59ba47c17ca6c6479015c1d5b8b8f6b9a");
} catch (EncodingException e) {
throw new RuntimeException(e);
}
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final String serialKey = "com.blizzard.messenger.AUTHENTICATOR_SERIAL";
final String secretKey = "com.blizzard.messenger.AUTHENTICATOR_DEVICE_SECRET";
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
String serial = "";
String secretValue = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(secretKey)) {
secretValue = entry.Value;
} else if (entry.Name.equals(serialKey)) {
serial = entry.Value;
}
}
if (secretValue == null) {
throw new DatabaseImporterException(String.format("Key not found: %s", secretKey));
}
return new BattleNetImporter.State(serial, secretValue);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final String _serial;
private final String _secretValue;
public State(String serial, String secretValue) {
super(false);
_serial = serial;
_secretValue = secretValue;
}
@Override
public Result convert() {
Result result = new Result();
try {
VaultEntry entry = convertEntry(_serial, _secretValue);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static VaultEntry convertEntry(String serial, String secretString) throws DatabaseImporterEntryException {
try {
if (!Strings.isNullOrEmpty(serial)) {
serial = unmask(serial);
}
byte[] secret = Hex.decode(unmask(secretString));
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, serial, "Battle.net");
} catch (OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, secretString);
}
}
private static String unmask(String s) throws EncodingException {
byte[] ds = Hex.decode(s);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ds.length; i++) {
char c = (char) (ds[i] ^ _key[i]);
sb.append(c);
}
return sb.toString();
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/BitwardenImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.simpleflatmapper.csv.CsvParser;
import org.simpleflatmapper.lightningcsv.Row;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
public class BitwardenImporter extends DatabaseImporter {
public BitwardenImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
String fileString;
try {
fileString = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
JSONObject obj = new JSONObject(fileString);
JSONArray array = obj.getJSONArray("items");
List entries = new ArrayList<>();
String entry;
for (int i = 0; i < array.length(); i++) {
entry = array.getJSONObject(i).getJSONObject("login").getString("totp");
if (!entry.isEmpty()) {
entries.add(entry);
}
}
return new BitwardenImporter.State(entries);
} catch (JSONException e) {
try {
Iterator rowIterator = CsvParser.separator(',').rowIterator(fileString);
List entries = new ArrayList<>();
rowIterator.forEachRemaining((row -> {
String entry = row.get("login_totp");
if (entry != null && !entry.isEmpty()) {
entries.add(entry);
}
}));
return new BitwardenImporter.State(entries);
} catch (IOException e2) {
throw new DatabaseImporterException(e2);
}
}
}
public static class State extends DatabaseImporter.State {
private final List _entries;
public State(List entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (String obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String obj) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = BitwardenImporter.parseUri(obj);
return new VaultEntry(info);
} catch (GoogleAuthInfoException | EncodingException | OtpInfoException | URISyntaxException e) {
throw new DatabaseImporterEntryException(e, obj);
}
}
}
private static GoogleAuthInfo parseUri(String s) throws EncodingException, OtpInfoException, URISyntaxException, GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (Objects.equals(uri.getScheme(), "steam")) {
String secretString = uri.getAuthority();
if (secretString == null) {
throw new GoogleAuthInfoException(uri, "Empty secret (empty authority)");
}
byte[] secret = Base32.decode(secretString);
return new GoogleAuthInfo(new SteamInfo(secret), "Steam account", "Steam");
}
return GoogleAuthInfo.parseUri(uri);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public abstract class DatabaseImporter {
private Context _context;
private static List _importers;
static {
// note: keep these lists sorted alphabetically
_importers = new ArrayList<>();
_importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false));
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Proton Authenticator", ProtonAuthenticatorImporter.class, R.string.importer_help_proton_authenticator, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
public DatabaseImporter(Context context) {
_context = context;
}
protected Context requireContext() {
return _context;
}
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = requireContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
public boolean isInstalledAppVersionSupported() {
return true;
}
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
public State read(InputStream stream) throws DatabaseImporterException {
return read(stream, false);
}
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile file = getAppPath();
file.setShell(shell);
try (InputStream stream = SuFileInputStream.open(file)) {
return read(stream, true);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static DatabaseImporter create(Context context, Class extends DatabaseImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static List getImporters(boolean isDirect) {
if (isDirect) {
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
}
return Collections.unmodifiableList(_importers);
}
public static class Definition implements Serializable {
private final String _name;
private final Class extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
/**
*
* @param name The name of the Authenticator the importer handles.
* @param type The class which does the importing.
* @param help The string that explains the type of file needed (and optionally where it can be obtained).
* @param supportsDirect Whether the importer can directly import the entries from the app's internal storage using root access.
*/
public Definition(String name, Class extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
_help = help;
_supportsDirect = supportsDirect;
}
public String getName() {
return _name;
}
public Class extends DatabaseImporter> getType() {
return _type;
}
public @StringRes int getHelp() {
return _help;
}
public boolean supportsDirect() {
return _supportsDirect;
}
}
public static abstract class State {
private boolean _encrypted;
public State(boolean encrypted) {
_encrypted = encrypted;
}
public boolean isEncrypted() {
return _encrypted;
}
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
if (!_encrypted) {
throw new RuntimeException("Attempted to decrypt a plain text database");
}
throw new UnsupportedOperationException();
}
public Result convert() throws DatabaseImporterException {
if (_encrypted) {
throw new RuntimeException("Attempted to convert database before decrypting it");
}
throw new UnsupportedOperationException();
}
}
public static class Result {
private UUIDMap _entries = new UUIDMap<>();
private UUIDMap _groups = new UUIDMap<>();
private List _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public UUIDMap getEntries() {
return _entries;
}
public UUIDMap getGroups() {
return _groups;
}
public List getErrors() {
return _errors;
}
}
public static abstract class DecryptListener {
protected abstract void onStateDecrypted(State state);
protected abstract void onError(Exception e);
protected abstract void onCanceled();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterEntryException.java
================================================
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;
}
public String getText() {
return _text;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/DatabaseImporterException.java
================================================
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterException extends Exception {
public DatabaseImporterException(Throwable cause) {
super(cause);
}
public DatabaseImporterException(String message) {
super(message);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/DuoImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.encoding.Base32;
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.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
public class DuoImporter extends DatabaseImporter {
private static final String _pkgName = "com.duosecurity.duomobile";
private static final String _subPath = "files/duokit/accounts.json";
public DuoImporter(Context context) {
super(context);
}
@Override
protected @NonNull SuFile getAppPath() throws DatabaseImporterException, NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected @NonNull State read(
@NonNull InputStream stream, boolean isInternal
) throws DatabaseImporterException {
try {
String contents = new String(IOUtils.readAll(stream), UTF_8);
return new DecryptedState(new JSONArray(contents));
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final JSONArray _array;
public DecryptedState(@NonNull JSONArray array) {
super(false);
_array = array;
}
@Override
public @NonNull Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entry = _array.getJSONObject(i);
try {
result.addEntry(convertEntry(entry));
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static @NonNull VaultEntry convertEntry(
@NonNull JSONObject entry
) throws DatabaseImporterEntryException {
try {
String label = entry.optString("name");
JSONObject otpData = entry.getJSONObject("otpGenerator");
byte[] secret = Base32.decode(otpData.getString("otpSecret"));
Long counter = otpData.has("counter") ? otpData.getLong("counter") : null;
OtpInfo otp = counter == null
? new TotpInfo(secret)
: new HotpInfo(secret, counter);
return new VaultEntry(otp, label, "");
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/EnteAuthImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class EnteAuthImporter extends DatabaseImporter {
public EnteAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
return importer.read(new ByteArrayInputStream(bytes), isInternal);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
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.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
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.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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 FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public FreeOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try (BufferedInputStream bufInStream = new BufferedInputStream(stream);
DataInputStream dataInStream = new DataInputStream(bufInStream)) {
dataInStream.mark(2);
int magic = dataInStream.readUnsignedShort();
dataInStream.reset();
if (magic == SerializedHashMapParser.MAGIC) {
return readV2(dataInStream);
} else {
return readV1(bufInStream);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
private DecryptedStateV1 readV1(InputStream stream) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
List entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (!entry.Name.equals("tokenOrder")) {
entries.add(new JSONObject(entry.Value));
}
}
return new DecryptedStateV1(entries);
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private EncryptedState readV2(DataInputStream stream) throws DatabaseImporterException {
try {
Map entries = SerializedHashMapParser.parse(stream);
JSONObject mkObj = new JSONObject(entries.get("masterKey"));
return new EncryptedState(mkObj, entries);
} catch (IOException | JSONException | SerializedHashMapParser.ParseException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private static final int MASTER_KEY_SIZE = 32 * 8;
private final String _mkAlgo;
private final String _mkCipher;
private final byte[] _mkCipherText;
private final byte[] _mkParameters;
private final byte[] _mkToken;
private final byte[] _mkSalt;
private final int _mkIterations;
private final Map _entries;
private EncryptedState(JSONObject mkObj, Map entries)
throws DatabaseImporterException, JSONException {
super(true);
_mkAlgo = mkObj.getString("mAlgorithm");
if (!_mkAlgo.equals("PBKDF2withHmacSHA1") && !_mkAlgo.equals("PBKDF2withHmacSHA512")) {
throw new DatabaseImporterException(String.format("Unexpected master key KDF: %s", _mkAlgo));
}
JSONObject keyObj = mkObj.getJSONObject("mEncryptedKey");
_mkCipher = keyObj.getString("mCipher");
if (!_mkCipher.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected master key cipher: %s", _mkCipher));
}
_mkCipherText = toBytes(keyObj.getJSONArray("mCipherText"));
_mkParameters = toBytes(keyObj.getJSONArray("mParameters"));
_mkToken = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
_mkSalt = toBytes(mkObj.getJSONArray("mSalt"));
_mkIterations = mkObj.getInt("mIterations");
_entries = entries;
}
public State decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = new PBKDFTask.Params(_mkAlgo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
SecretKey passKey = PBKDFTask.deriveKey(params);
return decrypt(passKey);
}
public State decrypt(SecretKey passKey) throws DatabaseImporterException {
byte[] masterKeyBytes;
try {
byte[] nonce = parseNonce(_mkParameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(_mkCipher);
cipher.init(Cipher.DECRYPT_MODE, passKey, spec);
cipher.updateAAD(_mkToken);
masterKeyBytes = cipher.doFinal(_mkCipherText);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
IllegalBlockSizeException | InvalidKeyException |
InvalidAlgorithmParameterException | IOException e) {
throw new DatabaseImporterException(e);
}
SecretKey masterKey = new SecretKeySpec(masterKeyBytes, 0, masterKeyBytes.length, "AES");
return new DecryptedStateV2(_entries, masterKey);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.importer_warning_title_freeotp2)
.setMessage(R.string.importer_warning_message_freeotp2)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, password -> {
PBKDFTask.Params params = getKeyDerivationParams(password, _mkAlgo);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
State state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog1 -> listener.onCanceled());
})
.create());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password, String algo) {
return new PBKDFTask.Params(algo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
}
}
public static class DecryptedStateV2 extends DatabaseImporter.State {
private final Map _entries;
private final SecretKey _masterKey;
public DecryptedStateV2(Map entries, SecretKey masterKey) {
super(false);
_entries = entries;
_masterKey = masterKey;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (Map.Entry entry : _entries.entrySet()) {
if (entry.getKey().endsWith("-token") || entry.getKey().equals("masterKey")) {
continue;
}
try {
JSONObject encObj = new JSONObject(entry.getValue());
String tokenKey = String.format("%s-token", entry.getKey());
JSONObject tokenObj = new JSONObject(_entries.get(tokenKey));
VaultEntry vaultEntry = convertEntry(encObj, tokenObj);
result.addEntry(vaultEntry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
} catch (JSONException ignored) {
}
}
return result;
}
private VaultEntry convertEntry(JSONObject encObj, JSONObject tokenObj)
throws DatabaseImporterEntryException {
try {
JSONObject keyObj = new JSONObject(encObj.getString("key"));
String cipherName = keyObj.getString("mCipher");
if (!cipherName.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected cipher: %s", cipherName));
}
byte[] cipherText = toBytes(keyObj.getJSONArray("mCipherText"));
byte[] parameters = toBytes(keyObj.getJSONArray("mParameters"));
byte[] token = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
byte[] nonce = parseNonce(parameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(cipherName);
cipher.init(Cipher.DECRYPT_MODE, _masterKey, spec);
cipher.updateAAD(token);
byte[] secretBytes = cipher.doFinal(cipherText);
JSONArray secretArray = new JSONArray();
for (byte b : secretBytes) {
secretArray.put(b);
}
tokenObj.put("secret", secretArray);
return DecryptedStateV1.convertEntry(tokenObj);
} catch (DatabaseImporterException | JSONException | NoSuchAlgorithmException |
NoSuchPaddingException | InvalidAlgorithmParameterException |
InvalidKeyException | BadPaddingException | IllegalBlockSizeException |
IOException e) {
throw new DatabaseImporterEntryException(e, tokenObj.toString());
}
}
}
public static class DecryptedStateV1 extends DatabaseImporter.State {
private final List _entries;
public DecryptedStateV1(List entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
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 {
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.optString("algo", OtpInfo.DEFAULT_ALGORITHM);
int digits = obj.optInt("digits", OtpInfo.DEFAULT_DIGITS);
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
OtpInfo info;
switch (type) {
case "totp":
int period = obj.optInt("period", TotpInfo.DEFAULT_PERIOD);
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
info = new TotpInfo(secret, algo, digits, period);
}
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static byte[] parseNonce(byte[] parameters) throws IOException {
ASN1Primitive prim = ASN1Sequence.fromByteArray(parameters);
if (prim instanceof ASN1OctetString) {
return ((ASN1OctetString) prim).getOctets();
}
if (prim instanceof ASN1Sequence) {
for (ASN1Encodable enc : (ASN1Sequence) prim) {
if (enc instanceof ASN1OctetString) {
return ((ASN1OctetString) enc).getOctets();
}
}
}
throw new IOException("Unable to find nonce in parameters");
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
bytes[i] = (byte)array.getInt(i);
}
return bytes;
}
private static class SerializedHashMapParser {
private static final int MAGIC = 0xaced;
private static final int VERSION = 5;
private static final long SERIAL_VERSION_UID = 362498820763181265L;
private static final byte TC_NULL = 0x70;
private static final byte TC_CLASSDESC = 0x72;
private static final byte TC_OBJECT = 0x73;
private static final byte TC_STRING = 0x74;
private SerializedHashMapParser() {
}
public static Map parse(DataInputStream inStream)
throws IOException, ParseException {
Map map = new HashMap<>();
// Read/validate the magic number and version
int magic = inStream.readUnsignedShort();
int version = inStream.readUnsignedShort();
if (magic != MAGIC || version != VERSION) {
throw new ParseException("Not a serialized Java Object");
}
// Read the class descriptor info for HashMap
byte b = inStream.readByte();
if (b != TC_OBJECT) {
throw new ParseException("Expected an object, found: " + b);
}
b = inStream.readByte();
if (b != TC_CLASSDESC) {
throw new ParseException("Expected a class desc, found: " + b);
}
parseClassDescriptor(inStream);
// Not interested in the capacity of the map
inStream.readInt();
// Read the number of elements in the HashMap
int size = inStream.readInt();
// Parse each key-value pair in the map
for (int i = 0; i < size; i++) {
String key = parseStringObject(inStream);
String value = parseStringObject(inStream);
map.put(key, value);
}
return map;
}
private static void parseClassDescriptor(DataInputStream inputStream)
throws IOException, ParseException {
// Check whether we're dealing with a HashMap and a version we support
String className = parseUTF(inputStream);
if (!className.equals(HashMap.class.getName())) {
throw new ParseException(String.format("Unexpected class name: %s", className));
}
long serialVersionUID = inputStream.readLong();
if (serialVersionUID != SERIAL_VERSION_UID) {
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
}
// Read past all of the fields in the class
byte fieldDescriptor = inputStream.readByte();
if (fieldDescriptor == TC_NULL) {
return;
}
int totalFieldSkip = 0;
int fieldCount = inputStream.readUnsignedShort();
for (int i = 0; i < fieldCount; i++) {
char fieldType = (char) inputStream.readByte();
parseUTF(inputStream);
switch (fieldType) {
case 'F': // float (4 bytes)
case 'I': // int (4 bytes)
totalFieldSkip += 4;
break;
default:
throw new ParseException(String.format("Unexpected field type: %s", fieldType));
}
}
inputStream.skipBytes(totalFieldSkip);
// Not sure what these bytes are, just skip them
inputStream.skipBytes(4);
}
private static String parseStringObject(DataInputStream inputStream)
throws IOException, ParseException {
byte objectType = inputStream.readByte();
if (objectType != TC_STRING) {
throw new ParseException(String.format("Expected a string object, found: %d", objectType));
}
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static String parseUTF(DataInputStream inputStream) throws IOException {
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/FreeOtpPlusImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FreeOtpPlusImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.liberty.android.freeotpplus";
public FreeOtpPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
State state;
if (isInternal) {
state = new FreeOtpImporter(requireContext()).read(stream);
} else {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
List entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.DecryptedStateV1(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
return state;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
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.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
SuFile file = getAppPath(_pkgName, _subPath);
return file;
}
@Override
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode <= 5000100;
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List entries = helper.read(Entry.class, stream, "accounts");
return new State(entries, context);
}
@Override
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List entries = helper.read(Entry.class, path, "accounts");
return new State(entries, context);
}
public static class State extends DatabaseImporter.State {
private List _entries;
private Context _context;
private State(List entries, Context context) {
super(false);
_entries = entries;
_context = context;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
VaultEntry entry = convertEntry(sqlEntry, _context);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry, Context context) throws DatabaseImporterEntryException {
try {
if (entry.isEncrypted()) {
throw new DatabaseImporterException(context.getString(R.string.importer_encrypted_exception_google_authenticator, entry.getEmail()));
}
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new VaultEntry(info, name, entry.getIssuer());
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private boolean _isEncrypted;
private String _secret;
private String _email;
private String _issuer;
private long _counter;
public Entry(Cursor cursor) {
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");
_isEncrypted = (cursor.getColumnIndex("isencrypted") != -1 && SqlImporterHelper.getInt(cursor, "isencrypted") > 0);
}
public int getType() {
return _type;
}
public boolean isEncrypted() {
return _isEncrypted;
}
public String getSecret() {
return _secret;
}
public String getEmail() {
return _email;
}
public String getIssuer() {
return _issuer;
}
public long getCounter() {
return _counter;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/GoogleAuthUriImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class GoogleAuthUriImporter extends DatabaseImporter {
public GoogleAuthUriImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (!line.isEmpty()) {
lines.add(line);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new GoogleAuthUriImporter.State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList _lines;
private State(ArrayList lines) {
super(false);
_lines = lines;
}
@Override
public DatabaseImporter.Result convert() {
DatabaseImporter.Result result = new DatabaseImporter.Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
return new VaultEntry(info);
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/MicrosoftAuthImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
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 com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
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 SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List entries = helper.read(Entry.class, path, "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 = GoogleAuthInfo.parseSecret(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, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
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;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/ProtonAuthenticatorImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
public class ProtonAuthenticatorImporter extends DatabaseImporter {
public ProtonAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected @NonNull State read(@NonNull InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String contents = new String(IOUtils.readAll(stream), UTF_8);
JSONObject json = new JSONObject(contents);
return new DecryptedState(json);
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final JSONObject _json;
public DecryptedState(@NonNull JSONObject json) {
super(false);
_json = json;
}
@Override
public @NonNull Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
JSONArray entries = _json.getJSONArray("entries");
for (int i = 0; i < entries.length(); i++) {
JSONObject entry = entries.getJSONObject(i);
try {
result.addEntry(convertEntry(entry));
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static @NonNull VaultEntry convertEntry(@NonNull JSONObject entry) throws DatabaseImporterEntryException {
try {
JSONObject content = entry.getJSONObject("content");
String name = content.getString("name");
String uriString = content.getString("uri");
Uri uri = Uri.parse(uriString);
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
OtpInfo otp = info.getOtpInfo();
return new VaultEntry(otp, name, info.getIssuer());
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, uriString);
}
} catch (JSONException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/SqlImporterHelper.java
================================================
package com.beemdevelopment.aegis.importers;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.common.io.Files;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
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;
public class SqlImporterHelper {
private Context _context;
public SqlImporterHelper(Context context) {
_context = context;
}
public List read(Class type, SuFile path, String table) throws DatabaseImporterException {
File dir = Files.createTempDir();
File mainFile = new File(dir, path.getName());
List fileCopies = new ArrayList<>();
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
// create temporary copies of the database files so that SQLiteDatabase can open them
File fileCopy = null;
try (InputStream inStream = SuFileInputStream.open(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
}
fileCopies.add(fileCopy);
} catch (IOException e) {
if (fileCopy != null) {
fileCopy.delete();
}
for (File fileCopy2 : fileCopies) {
fileCopy2.delete();
}
throw new DatabaseImporterException(e);
}
}
try {
return read(type, mainFile, table);
} finally {
for (File fileCopy : fileCopies) {
fileCopy.delete();
}
}
}
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
}
return files;
}
public List read(Class type, InputStream inStream, String table) throws DatabaseImporterException {
File file = null;
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)) {
IOUtils.copy(inStream, out);
}
} catch (IOException e) {
if (file != null) {
file.delete();
}
throw new DatabaseImporterException(e);
}
try {
return read(type, file, table);
} finally {
// always delete the temporary file
file.delete();
}
}
private List read(Class type, File file, String table) throws DatabaseImporterException {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
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);
}
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
@SuppressLint("Range")
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
public static abstract class Entry {
public Entry(Cursor cursor) {
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/SteamImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
public SteamImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
// NOTE: this assumes that a global root shell has already been obtained by the caller
SuFile path = getAppPath(_pkgName, _subDir);
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
}
// TODO: handle multiple files (can this even occur?)
return files[0];
}
@Override
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode < 7460894;
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
List objs = new ArrayList<>();
if (obj.has("accounts")) {
JSONObject accounts = obj.getJSONObject("accounts");
Iterator keys = accounts.keys();
while (keys.hasNext()) {
String key = keys.next();
objs.add(accounts.getJSONObject(key));
}
} else {
objs.add(obj);
}
return new State(objs);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final List _objs;
private State(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 {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/StratumImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
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.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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;
public class StratumImporter extends DatabaseImporter {
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "com.stratumauth.app";
private static final String PKG_DB_PATH = "databases/authenticator.db3";
private enum Algorithm {
SHA1,
SHA256,
SHA512
}
public StratumImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(PKG_NAME, PKG_DB_PATH);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
return isInternal ? readInternal(stream) : readExternal(stream);
}
private State readInternal(InputStream stream) throws DatabaseImporterException {
List entries = new SqlImporterHelper(requireContext()).read(SqlEntry.class, stream, "authenticator");
return new SqlState(entries);
}
private static State readExternal(InputStream stream) throws DatabaseImporterException {
byte[] data;
try {
data = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return new JsonState(new JSONObject(new String(data, StandardCharsets.UTF_8)));
} catch (JSONException e) {
return readEncrypted(new DataInputStream(new ByteArrayInputStream(data)));
}
}
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
try {
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
stream.readFully(headerBytes);
String header = new String(headerBytes, StandardCharsets.UTF_8);
switch (header) {
case HEADER:
return EncryptedState.parseHeader(stream);
case HEADER_LEGACY:
return LegacyEncryptedState.parseHeader(stream);
default:
throw new DatabaseImporterException("Invalid file header");
}
} catch (UTFDataFormatException e) {
throw new DatabaseImporterException("Invalid file header");
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new DatabaseImporterException(e);
}
}
private static OtpInfo parseOtpInfo(int type, byte[] secret, Algorithm algo, int digits, int period, int counter)
throws OtpInfoException, DatabaseImporterEntryException {
switch (type) {
case 1:
return new HotpInfo(secret, algo.name(), digits, counter);
case 2:
return new TotpInfo(secret, algo.name(), digits, period);
case 4:
return new SteamInfo(secret, algo.name(), digits, period);
default:
throw new DatabaseImporterEntryException(String.format("Unsupported otp type: %d", type), null);
}
}
static class EncryptedState extends State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
private static final int PARALLELISM = 4;
private static final int ITERATIONS = 3;
private static final int SALT_SIZE = 16;
private static final int IV_SIZE = 12;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public EncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
Argon2Task.Params params = getKeyDerivationParams(password);
SecretKey key = Argon2Task.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private Argon2Task.Params getKeyDerivationParams(char[] password) {
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryPowOfTwo(MEMORY_COST)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}
private static EncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
byte[] iv = new byte[IV_SIZE];
stream.readFully(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
static class LegacyEncryptedState extends State {
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final int SALT_SIZE = 20;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
}
private static LegacyEncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
private static class JsonState extends State {
private final JSONObject _obj;
public JsonState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
try {
JSONArray array = _obj.getJSONArray("Authenticators");
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
try {
res.addEntry(convertEntry(obj));
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return res;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int type = obj.getInt("Type");
String issuer = obj.getString("Issuer");
Object nullableUsername = obj.get("Username");
String username = nullableUsername == JSONObject.NULL ? "" : nullableUsername.toString();
byte[] secret = Base32.decode(obj.getString("Secret"));
Algorithm algo = Algorithm.values()[obj.getInt("Algorithm")];
int digits = obj.getInt("Digits");
int period = obj.getInt("Period");
int counter = obj.getInt("Counter");
OtpInfo info = parseOtpInfo(type, secret, algo, digits, period, counter);
return new VaultEntry(info, username, issuer);
} catch (OtpInfoException | EncodingException | JSONException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
private static class SqlState extends State {
private final List _entries;
public SqlState(List entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
for (SqlEntry entry : _entries) {
try {
res.addEntry(entry.convert());
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
return res;
}
}
private static class SqlEntry extends SqlImporterHelper.Entry {
private final int _type;
private final String _issuer;
private final String _username;
private final String _secret;
private final Algorithm _algo;
private final int _digits;
private final int _period;
private final int _counter;
public SqlEntry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_issuer = SqlImporterHelper.getString(cursor, "issuer");
_username = SqlImporterHelper.getString(cursor, "username");
_secret = SqlImporterHelper.getString(cursor, "secret");
_algo = Algorithm.values()[SqlImporterHelper.getInt(cursor, "algorithm")];
_digits = SqlImporterHelper.getInt(cursor, "digits");
_period = SqlImporterHelper.getInt(cursor, "period");
_counter = SqlImporterHelper.getInt(cursor, "counter");
}
public VaultEntry convert() throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(_secret);
OtpInfo info = parseOtpInfo(_type, secret, _algo, _digits, _period, _counter);
return new VaultEntry(info, _username, _issuer);
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/TotpAuthenticatorImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
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.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
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.io.InputStream;
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 SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, 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 = IOUtils.readAll(stream);
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) {
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected 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/PKCS5Padding");
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 MaterialAlertDialogBuilder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
}, dialog1 -> listener.onCanceled());
})
.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);
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());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/TwoFASImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
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.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
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 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.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class TwoFASImporter extends DatabaseImporter {
private static final int ITERATION_COUNT = 10_000;
private static final int KEY_SIZE = 256; // bits
public TwoFASImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 4) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
String encryptedString = JsonUtils.optString(obj, "servicesEncrypted");
if (encryptedString == null) {
JSONArray array = obj.getJSONArray("services");
List entries = arrayToList(array);
return new DecryptedState(entries);
}
String[] parts = encryptedString.split(":");
if (parts.length < 3) {
throw new DatabaseImporterException(String.format("Unexpected format of encrypted data (parts: %d)", parts.length));
}
byte[] data = Base64.decode(parts[0]);
byte[] salt = Base64.decode(parts[1]);
byte[] iv = Base64.decode(parts[2]);
return new EncryptedState(data, salt, iv);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List arrayToList(JSONArray array) throws JSONException {
List list = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
list.add(array.getJSONObject(i));
}
return list;
}
public static class EncryptedState extends State {
private final byte[] _data;
private final byte[] _salt;
private final byte[] _iv;
private EncryptedState(byte[] data, byte[] salt, byte[] iv) {
super(true);
_data = data;
_salt = salt;
_iv = iv;
}
private SecretKey deriveKey(char[] password)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, _salt, ITERATION_COUNT, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
SecretKey key = deriveKey(password);
Cipher cipher = CryptoUtils.createDecryptCipher(key, _iv);
byte[] decrypted = cipher.doFinal(_data);
String json = new String(decrypted, StandardCharsets.UTF_8);
return new DecryptedState(arrayToList(new JSONArray(json)));
} catch (BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| NoSuchPaddingException
| InvalidKeyException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_2fas_message, 0, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final List _entries;
public DecryptedState(List entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
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 {
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = obj.optString("name");
if (Strings.isNullOrEmpty(issuer)) {
issuer = info.optString("issuer");
}
String name = info.optString("account");
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
OtpInfo otp;
String tokenType = JsonUtils.optString(info, "tokenType");
if (tokenType == null || tokenType.equals("TOTP")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new TotpInfo(secret, algorithm, digits, period);
} else if (tokenType.equals("HOTP")) {
long counter = info.optLong("counter", 0);
otp = new HotpInfo(secret, algorithm, digits, counter);
} else if (tokenType.equals("STEAM")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new SteamInfo(secret, algorithm, digits, period);
} else {
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
}
return new VaultEntry(otp, name, issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/importers/WinAuthImporter.java
================================================
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private DatabaseImporter.State _state;
private State(DatabaseImporter.State state) {
super(false);
_state = state;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = _state.convert();
for (VaultEntry entry : result.getEntries()) {
entry.setIssuer(entry.getName());
entry.setName("WinAuth");
}
return result;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.GoogleAuthProtos;
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.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class GoogleAuthInfo implements Transferable, Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
private OtpInfo _info;
private String _accountName;
private String _issuer;
public GoogleAuthInfo(OtpInfo info, String accountName, String issuer) {
_info = info;
_accountName = accountName;
_issuer = issuer;
}
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !(scheme.equals(SCHEME) || scheme.equals(MotpInfo.SCHEME))) {
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
}
byte[] secret;
try {
secret = (scheme.equals(MotpInfo.SCHEME)) ? Hex.decode(encodedSecret) : parseSecret(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException(uri, "Bad secret", e);
}
if (secret.length == 0) {
throw new GoogleAuthInfoException(uri, "Secret is empty");
}
OtpInfo info;
String issuer = "";
try {
String type = (scheme.equals(MotpInfo.SCHEME)) ? MotpInfo.ID : uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
case "totp":
TotpInfo totpInfo = new TotpInfo(secret);
String period = uri.getQueryParameter("period");
if (period != null) {
totpInfo.setPeriod(Integer.parseInt(period));
}
info = totpInfo;
break;
case "steam":
SteamInfo steamInfo = new SteamInfo(secret);
period = uri.getQueryParameter("period");
if (period != null) {
steamInfo.setPeriod(Integer.parseInt(period));
}
info = steamInfo;
break;
case "hotp":
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
case YandexInfo.HOST_ID:
String pin = uri.getQueryParameter("pin");
if (pin != null) {
pin = new String(parseSecret(pin), StandardCharsets.UTF_8);
}
info = new YandexInfo(secret, pin);
issuer = info.getType();
break;
case MotpInfo.ID:
info = new MotpInfo(secret);
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException | NumberFormatException | EncodingException e) {
throw new GoogleAuthInfoException(uri, e);
}
// provider info used to disambiguate accounts
String path = uri.getPath();
String label = path != null && path.length() > 0 ? path.substring(1) : "";
String accountName = "";
if (label.contains(":")) {
// a label can only contain one colon
// it's ok to fail if that's not the case
String[] strings = label.split(":");
if (strings.length == 2) {
issuer = strings[0];
accountName = strings[1];
} else {
// at this point, just dump the whole thing into the accountName
accountName = label;
}
} else {
// label only contains the account name
// grab the issuer's info from the 'issuer' parameter if it's present
String issuerParam = uri.getQueryParameter("issuer");
if (issuer.isEmpty()) {
issuer = issuerParam != null ? issuerParam : "";
}
accountName = label;
}
// just use the defaults if these parameters aren't set
try {
String algorithm = uri.getQueryParameter("algorithm");
if (algorithm != null) {
info.setAlgorithm(algorithm);
}
String digits = uri.getQueryParameter("digits");
if (digits != null) {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(uri, e);
}
return new GoogleAuthInfo(info, accountName, issuer);
}
/**
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
*/
public static byte[] parseSecret(String s) throws EncodingException {
s = s.trim().replace("-", "").replace(" ", "");
return Base32.decode(s);
}
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException(uri, "Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException(uri, "Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
try {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(uri, e);
}
List infos = new ArrayList<>();
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
int digits;
switch (params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = TotpInfo.DEFAULT_DIGITS;
break;
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
}
String algo;
switch (params.getAlgorithm()) {
case ALGORITHM_UNSPECIFIED:
// intentional fallthrough
case ALGORITHM_SHA1:
algo = "SHA1";
break;
case ALGORITHM_SHA256:
algo = "SHA256";
break;
case ALGORITHM_SHA512:
algo = "SHA512";
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
}
byte[] secret = params.getSecret().toByteArray();
if (secret.length == 0) {
throw new GoogleAuthInfoException(uri, "Secret is empty");
}
switch (params.getType()) {
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
case OTP_TYPE_TOTP:
otp = new TotpInfo(secret, algo, digits, TotpInfo.DEFAULT_PERIOD);
break;
case OTP_TYPE_HOTP:
otp = new HotpInfo(secret, algo, digits, params.getCounter());
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e) {
throw new GoogleAuthInfoException(uri, e);
}
String name = params.getName();
String issuer = params.getIssuer();
int colonI = name.indexOf(':');
if (issuer.isEmpty() && colonI != -1) {
issuer = name.substring(0, colonI);
name = name.substring(colonI + 1);
}
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
infos.add(info);
}
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
}
public OtpInfo getOtpInfo() {
return _info;
}
@Override
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
if (_info instanceof MotpInfo) {
builder.scheme(MotpInfo.SCHEME);
builder.appendQueryParameter("secret", Hex.encode(_info.getSecret()));
} else {
builder.scheme(SCHEME);
if (_info instanceof TotpInfo) {
if (_info instanceof SteamInfo) {
builder.authority("steam");
} else if (_info instanceof YandexInfo) {
builder.authority(YandexInfo.HOST_ID);
} else {
builder.authority("totp");
}
builder.appendQueryParameter("period", Integer.toString(((TotpInfo) _info).getPeriod()));
} else if (_info instanceof HotpInfo) {
builder.authority("hotp");
builder.appendQueryParameter("counter", Long.toString(((HotpInfo) _info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
}
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
if (_info instanceof YandexInfo) {
builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPin()));
}
}
if (_issuer != null && !_issuer.equals("")) {
builder.path(String.format("%s:%s", _issuer, _accountName));
builder.appendQueryParameter("issuer", _issuer);
} else {
builder.path(_accountName);
}
return builder.build();
}
public String getIssuer() {
return _issuer;
}
public String getAccountName() {
return _accountName;
}
public static class Export implements Transferable, Serializable {
private int _batchId;
private int _batchIndex;
private int _batchSize;
private List _entries;
public Export(List entries, int batchId, int batchIndex, int batchSize) {
_batchId = batchId;
_batchIndex = batchIndex;
_batchSize = batchSize;
_entries = entries;
}
public List getEntries() {
return _entries;
}
public int getBatchSize() {
return _batchSize;
}
public int getBatchIndex() {
return _batchIndex;
}
public int getBatchId() {
return _batchId;
}
public static List getMissingIndices(@NonNull List exports) throws IllegalArgumentException {
if (!isSingleBatch(exports)) {
throw new IllegalArgumentException("Export list contains entries from different batches");
}
List indicesMissing = new ArrayList<>();
if (exports.isEmpty()) {
return indicesMissing;
}
Set indicesPresent = exports.stream()
.map(Export::getBatchIndex)
.collect(Collectors.toSet());
for (int i = 0; i < exports.get(0).getBatchSize(); i++) {
if (!indicesPresent.contains(i)) {
indicesMissing.add(i);
}
}
return indicesMissing;
}
public static boolean isSingleBatch(@NonNull List exports) {
if (exports.isEmpty()) {
return true;
}
int batchId = exports.get(0).getBatchId();
for (Export export : exports) {
if (export.getBatchId() != batchId) {
return false;
}
}
return true;
}
@Override
public Uri getUri() throws GoogleAuthInfoException {
GoogleAuthProtos.MigrationPayload.Builder builder = GoogleAuthProtos.MigrationPayload.newBuilder();
builder.setBatchId(_batchId)
.setBatchIndex(_batchIndex)
.setBatchSize(_batchSize)
.setVersion(1);
for (GoogleAuthInfo info: _entries) {
GoogleAuthProtos.MigrationPayload.OtpParameters.Builder parameters = GoogleAuthProtos.MigrationPayload.OtpParameters.newBuilder()
.setSecret(ByteString.copyFrom(info.getOtpInfo().getSecret()))
.setName(info.getAccountName())
.setIssuer(info.getIssuer());
switch (info.getOtpInfo().getAlgorithm(false)) {
case "SHA1":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA1);
break;
case "SHA256":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA256);
break;
case "SHA512":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_SHA512);
break;
case "MD5":
parameters.setAlgorithm(GoogleAuthProtos.MigrationPayload.Algorithm.ALGORITHM_MD5);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported Algorithm: %s", info.getOtpInfo().getAlgorithm(false)));
}
switch (info.getOtpInfo().getDigits()) {
case 6:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_SIX);
break;
case 8:
parameters.setDigits(GoogleAuthProtos.MigrationPayload.DigitCount.DIGIT_COUNT_EIGHT);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Unsupported number of digits: %s", info.getOtpInfo().getDigits()));
}
switch (info.getOtpInfo().getType().toLowerCase()) {
case HotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_HOTP);
parameters.setCounter(((HotpInfo) info.getOtpInfo()).getCounter());
break;
case TotpInfo.ID:
parameters.setType(GoogleAuthProtos.MigrationPayload.OtpType.OTP_TYPE_TOTP);
break;
default:
throw new GoogleAuthInfoException(info.getUri(), String.format("Type unsupported by GoogleAuthProtos: %s", info.getOtpInfo().getType()));
}
builder.addOtpParameters(parameters.build());
}
Uri.Builder exportUriBuilder = new Uri.Builder()
.scheme(SCHEME_EXPORT)
.authority("offline");
String data = Base64.encode(builder.build().toByteArray());
exportUriBuilder.appendQueryParameter("data", data);
return exportUriBuilder.build();
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfoException.java
================================================
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public class GoogleAuthInfoException extends Exception {
private final Uri _uri;
public GoogleAuthInfoException(Uri uri, Throwable cause) {
super(cause);
_uri = uri;
}
public GoogleAuthInfoException(Uri uri, String message) {
super(message);
_uri = uri;
}
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
super(message, cause);
_uri = uri;
}
/**
* Reports whether the scheme of the URI is phonefactor://.
*/
public boolean isPhoneFactor() {
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
}
@Override
public String getMessage() {
Throwable cause = getCause();
if (cause == null
|| this == cause
|| (super.getMessage() != null && super.getMessage().equals(cause.getMessage()))) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/HotpInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
public static final String ID = "hotp";
public static final int DEFAULT_COUNTER = 0;
private long _counter;
public HotpInfo(byte[] secret, long counter) throws OtpInfoException {
super(secret);
setCounter(counter);
}
public HotpInfo(byte[] secret) throws OtpInfoException {
this(secret, DEFAULT_COUNTER);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
super(secret, algorithm, digits);
setCounter(counter);
}
@Override
public String getOtp() throws OtpInfoException {
checkSecret();
try {
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
@Override
public String getTypeId() {
return ID;
}
@Override
public JSONObject toJson() {
JSONObject obj = super.toJson();
try {
obj.put("counter", getCounter());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public long getCounter() {
return _counter;
}
public static boolean isCounterValid(long counter) {
return counter >= 0;
}
public void setCounter(long counter) throws OtpInfoException {
if (!isCounterValid(counter)) {
throw new OtpInfoException(String.format("bad counter: %d", counter));
}
_counter = counter;
}
public void incrementCounter() throws OtpInfoException {
setCounter(getCounter() + 1);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof HotpInfo)) {
return false;
}
HotpInfo info = (HotpInfo) o;
return super.equals(o) && getCounter() == info.getCounter();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/MotpInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.crypto.otp.MOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
public class MotpInfo extends TotpInfo {
public static final String ID = "motp";
public static final String SCHEME = "motp";
public static final String ALGORITHM = "MD5";
public static final int PERIOD = 10;
public static final int DIGITS = 6;
private String _pin;
public MotpInfo(@NonNull byte[] secret) throws OtpInfoException {
this(secret, null);
}
public MotpInfo(byte[] secret, String pin) throws OtpInfoException {
super(secret, ALGORITHM, DIGITS, PERIOD);
setPin(pin);
}
@Override
public String getOtp(long time) {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin(), time);
return otp.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getTypeId() {
return ID;
}
@Override
public JSONObject toJson() {
JSONObject result = super.toJson();
try {
result.put("pin", getPin());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return result;
}
@Nullable
public String getPin() {
return _pin;
}
public void setPin(@NonNull String pin) {
this._pin = pin;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof MotpInfo)) {
return false;
}
MotpInfo info = (MotpInfo) o;
return super.equals(o) && Objects.equals(getPin(), info.getPin());
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Locale;
public abstract class OtpInfo implements Serializable {
public static final int DEFAULT_DIGITS = 6;
public static final String DEFAULT_ALGORITHM = "SHA1";
private byte[] _secret;
private String _algorithm;
private int _digits;
public OtpInfo(byte[] secret) throws OtpInfoException {
this(secret, DEFAULT_ALGORITHM, DEFAULT_DIGITS);
}
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
setSecret(secret);
setAlgorithm(algorithm);
setDigits(digits);
}
public abstract String getOtp() throws OtpInfoException;
protected void checkSecret() throws OtpInfoException {
if (getSecret().length == 0) {
throw new OtpInfoException("Secret is empty");
}
}
public abstract String getTypeId();
public String getType() {
return getTypeId().toUpperCase(Locale.ROOT);
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("secret", Base32.encode(getSecret()));
obj.put("algo", getAlgorithm(false));
obj.put("digits", getDigits());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public byte[] getSecret() {
return _secret;
}
public String getAlgorithm(boolean java) {
if (java) {
return "Hmac" + _algorithm;
}
return _algorithm;
}
public int getDigits() {
return _digits;
}
public void setSecret(byte[] secret) {
_secret = secret;
}
public static boolean isAlgorithmValid(String algorithm) {
return algorithm.equals("SHA1") || algorithm.equals("SHA256") ||
algorithm.equals("SHA512") || algorithm.equals("MD5");
}
public void setAlgorithm(String algorithm) throws OtpInfoException {
if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4);
}
algorithm = algorithm.toUpperCase(Locale.ROOT);
if (!isAlgorithmValid(algorithm)) {
throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm));
}
_algorithm = algorithm;
}
public static boolean isDigitsValid(int digits) {
// allow a max of 10 digits, as truncation will only extract 31 bits
return digits > 0 && digits <= 10;
}
public void setDigits(int digits) throws OtpInfoException {
if (!isDigitsValid(digits)) {
throw new OtpInfoException(String.format("unsupported amount of digits: %d", digits));
}
_digits = digits;
}
public static OtpInfo fromJson(String type, JSONObject obj) throws OtpInfoException {
OtpInfo info;
try {
byte[] secret = Base32.decode(obj.getString("secret"));
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
// Special case to work around a bug where a user could accidentally
// set the hash algorithm of a non-mOTP entry to MD5
if (!type.equals(MotpInfo.ID) && algo.equals("MD5")) {
algo = DEFAULT_ALGORITHM;
}
switch (type) {
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, obj.getInt("period"));
break;
case HotpInfo.ID:
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case YandexInfo.ID:
info = new YandexInfo(secret, obj.getString("pin"));
break;
case MotpInfo.ID:
info = new MotpInfo(secret, obj.getString("pin"));
break;
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
} catch (EncodingException | JSONException e) {
throw new OtpInfoException(e);
}
return info;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof OtpInfo)) {
return false;
}
OtpInfo info = (OtpInfo) o;
return getTypeId().equals(info.getTypeId())
&& Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfoException.java
================================================
package com.beemdevelopment.aegis.otp;
public class OtpInfoException extends Exception {
public OtpInfoException(Throwable cause) {
super(cause);
}
public OtpInfoException(String message) {
super(message);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/SteamInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
public static final int DIGITS = 5;
public SteamInfo(byte[] secret) throws OtpInfoException {
super(secret, OtpInfo.DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
}
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
super(secret, algorithm, digits, period);
}
@Override
public String getOtp(long time) throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toSteamString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getTypeId() {
return ID;
}
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/TotpInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
public static final String ID = "totp";
public static final int DEFAULT_PERIOD = 30;
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
super(secret);
setPeriod(DEFAULT_PERIOD);
}
public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
super(secret, algorithm, digits);
setPeriod(period);
}
@Override
public String getOtp() throws OtpInfoException {
return getOtp(System.currentTimeMillis() / 1000);
}
public String getOtp(long time) throws OtpInfoException {
checkSecret();
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getTypeId() {
return ID;
}
@Override
public JSONObject toJson() {
JSONObject obj = super.toJson();
try {
obj.put("period", getPeriod());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public int getPeriod() {
return _period;
}
public static boolean isPeriodValid(int period) {
if (period <= 0) {
return false;
}
// check for the possibility of an overflow when converting to milliseconds
return period <= Integer.MAX_VALUE / 1000;
}
public void setPeriod(int period) throws OtpInfoException {
if (!isPeriodValid(period)) {
throw new OtpInfoException(String.format("bad period: %d", period));
}
_period = period;
}
public long getMillisTillNextRotation() {
return TotpInfo.getMillisTillNextRotation(_period);
}
public static long getMillisTillNextRotation(int period) {
long p = period * 1000;
return p - (System.currentTimeMillis() % p);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof TotpInfo)) {
return false;
}
TotpInfo info = (TotpInfo) o;
return super.equals(o) && getPeriod() == info.getPeriod();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/Transferable.java
================================================
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public interface Transferable {
Uri getUri() throws GoogleAuthInfoException;
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/otp/YandexInfo.java
================================================
package com.beemdevelopment.aegis.otp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.crypto.otp.YAOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
public class YandexInfo extends TotpInfo {
public static final String DEFAULT_ALGORITHM = "SHA256";
public static final int DIGITS = 8;
public static final int SECRET_LENGTH = 16;
public static final int SECRET_FULL_LENGTH = 26;
public static final String ID = "yandex";
public static final String HOST_ID = "yaotp";
@Nullable
private String _pin;
public YandexInfo(@NonNull byte[] secret) throws OtpInfoException {
this(secret, null);
}
public YandexInfo(@NonNull byte[] secret, @Nullable String pin) throws OtpInfoException {
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
setSecret(parseSecret(secret));
_pin = pin;
}
@Override
public String getOtp(long time) {
if (_pin == null) {
throw new IllegalStateException("PIN must be set before generating an OTP");
}
try {
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
throw new RuntimeException(e);
}
}
@Nullable
public String getPin() {
return _pin;
}
public void setPin(@NonNull String pin) {
_pin = pin;
}
@Override
public String getTypeId() {
return ID;
}
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
}
@Override
public JSONObject toJson() {
JSONObject result = super.toJson();
try {
result.put("pin", getPin());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof YandexInfo)) {
return false;
}
YandexInfo info = (YandexInfo) o;
return super.equals(o) && Objects.equals(getPin(), info.getPin());
}
public static byte[] parseSecret(byte[] secret) throws OtpInfoException {
validateSecret(secret);
if (secret.length != SECRET_LENGTH) {
return Arrays.copyOfRange(secret, 0, SECRET_LENGTH);
}
return secret;
}
/**
* Java implementation of ChecksumIsValid
* From: https://github.com/norblik/KeeYaOtp/blob/188a1a99f13f82e4ef8df8a1b9b9351ba236e2a1/KeeYaOtp/Core/Secret.cs
* License: GPLv3+
*/
public static void validateSecret(byte[] secret) throws OtpInfoException {
if (secret.length != SECRET_LENGTH && secret.length != SECRET_FULL_LENGTH) {
throw new OtpInfoException(String.format("Invalid Yandex secret length: %d bytes", secret.length));
}
// Secrets originating from a QR code do not have a checksum, so we assume those are valid
if (secret.length == SECRET_LENGTH) {
return;
}
char originalChecksum = (char) ((secret[secret.length - 2] & 0x0F) << 8 | secret[secret.length - 1] & 0xff);
char accum = 0;
int accumBits = 0;
int inputTotalBitsAvailable = secret.length * 8 - 12;
int inputIndex = 0;
int inputBitsAvailable = 8;
while (inputTotalBitsAvailable > 0) {
int requiredBits = 13 - accumBits;
if (inputTotalBitsAvailable < requiredBits) {
requiredBits = inputTotalBitsAvailable;
}
while (requiredBits > 0) {
int curInput = (secret[inputIndex] & (1 << inputBitsAvailable) - 1) & 0xff;
int bitsToRead = Math.min(requiredBits, inputBitsAvailable);
curInput >>= inputBitsAvailable - bitsToRead;
accum = (char) (accum << bitsToRead | curInput);
inputTotalBitsAvailable -= bitsToRead;
requiredBits -= bitsToRead;
inputBitsAvailable -= bitsToRead;
accumBits += bitsToRead;
if (inputBitsAvailable == 0) {
inputIndex += 1;
inputBitsAvailable = 8;
}
}
if (accumBits == 13) {
accum ^= 0b1_1000_1111_0011;
}
accumBits = 16 - getNumberOfLeadingZeros(accum);
}
if (accum != originalChecksum) {
throw new OtpInfoException("Yandex secret checksum invalid");
}
}
private static int getNumberOfLeadingZeros(char value) {
if (value == 0) {
return 16;
}
int n = 0;
if ((value & 0xFF00) == 0) {
n += 8;
value <<= 8;
}
if ((value & 0xF000) == 0) {
n += 4;
value <<= 4;
}
if ((value & 0xC000) == 0) {
n += 2;
value <<= 2;
}
if ((value & 0x8000) == 0) {
n++;
}
return n;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/receivers/QsTileRefreshReceiver.java
================================================
package com.beemdevelopment.aegis.receivers;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.TileService;
import com.beemdevelopment.aegis.services.LaunchAppTileService;
import com.beemdevelopment.aegis.services.LaunchScannerTileService;
public class QsTileRefreshReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null
|| (!intent.getAction().equals(ACTION_BOOT_COMPLETED)
&& !intent.getAction().equals(Intent.ACTION_USER_UNLOCKED))) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
TileService.requestListeningState(context,
new ComponentName(context, LaunchAppTileService.class));
TileService.requestListeningState(context,
new ComponentName(context, LaunchScannerTileService.class));
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/receivers/VaultLockReceiver.java
================================================
package com.beemdevelopment.aegis.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.vault.VaultManager;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class VaultLockReceiver extends BroadcastReceiver {
public static final String ACTION_LOCK_VAULT
= String.format("%s.LOCK_VAULT", BuildConfig.APPLICATION_ID);
@Inject
protected VaultManager _vaultManager;
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null
|| (!intent.getAction().equals(ACTION_LOCK_VAULT)
&& !intent.getAction().equals(Intent.ACTION_SCREEN_OFF))) {
return;
}
if (_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
_vaultManager.lock(false);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/services/LaunchAppTileService.java
================================================
package com.beemdevelopment.aegis.services;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.ui.MainActivity;
@RequiresApi(api = Build.VERSION_CODES.N)
public class LaunchAppTileService extends TileService {
@Override
public void onStartListening() {
super.onStartListening();
Tile tile = getQsTile();
if (tile != null) {
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
startActivityAndCollapse(pendingIntent);
} else {
startActivityAndCollapse(intent);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/services/LaunchScannerTileService.java
================================================
package com.beemdevelopment.aegis.services;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.ui.MainActivity;
@RequiresApi(api = Build.VERSION_CODES.N)
public class LaunchScannerTileService extends TileService {
@Override
public void onStartListening() {
super.onStartListening();
Tile tile = getQsTile();
if (tile != null) {
tile.setState(Tile.STATE_INACTIVE);
tile.updateTile();
}
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@Override
public void onClick() {
super.onClick();
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, flags);
startActivityAndCollapse(pendingIntent);
} else {
startActivityAndCollapse(intent);
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/services/NotificationService.java
================================================
package com.beemdevelopment.aegis.services;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
public class NotificationService extends Service {
private static final int NOTIFICATION_VAULT_UNLOCKED = 1;
private static final String CHANNEL_ID = "lock_status_channel";
@Override
public int onStartCommand(Intent intent,int flags, int startId){
super.onStartCommand(intent, flags, startId);
serviceMethod();
return Service.START_STICKY;
}
@SuppressLint("LaunchActivityFromNotification")
public void serviceMethod() {
int flags = PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE;
Intent intent = new Intent(this, VaultLockReceiver.class);
intent.setAction(VaultLockReceiver.ACTION_LOCK_VAULT);
intent.setPackage(BuildConfig.APPLICATION_ID);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, flags);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_aegis_notification)
.setContentTitle(getString(R.string.app_name_full))
.setContentText(getString(R.string.vault_unlocked_state))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setContentIntent(pendingIntent);
// NOTE: Disabled for now. See issue: #1047
//startForeground(NOTIFICATION_VAULT_UNLOCKED, builder.build());
}
@Override
public void onDestroy() {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(NOTIFICATION_VAULT_UNLOCKED);
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/AboutActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.StringRes;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
public class AboutActivity extends AegisActivity {
private static String GITHUB = "https://github.com/beemdevelopment/Aegis";
private static String WEBSITE_ALEXANDER = "https://alexbakker.me";
private static String GITHUB_MICHAEL = "https://github.com/michaelschattgen";
private static String MAIL_BEEMDEVELOPMENT = "beemdevelopment@gmail.com";
private static String WEBSITE_BEEMDEVELOPMENT = "https://beem.dev/";
private static String PLAYSTORE_BEEMDEVELOPMENT = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
View btnLicense = findViewById(R.id.btn_license);
btnLicense.setOnClickListener(v -> {
LicenseDialog.create()
.setTheme(_themeHelper.getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
btnThirdPartyLicenses.setOnClickListener(v -> {
Intent intent = new Intent(this, LicensesActivity.class);
startActivity(intent);
});
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
View btnAppVersion = findViewById(R.id.btn_app_version);
btnAppVersion.setOnClickListener(v -> {
copyToClipboard(getCurrentAppVersion(), R.string.version_copied);
});
View btnGithub = findViewById(R.id.btn_github);
btnGithub.setOnClickListener(v -> openUrl(GITHUB));
View btnAlexander = findViewById(R.id.btn_alexander);
btnAlexander.setOnClickListener(v -> openUrl(WEBSITE_ALEXANDER));
View btnMichael = findViewById(R.id.btn_michael);
btnMichael.setOnClickListener(v -> openUrl(GITHUB_MICHAEL));
View btnMail = findViewById(R.id.btn_email);
btnMail.setOnClickListener(v -> openMail(MAIL_BEEMDEVELOPMENT));
View btnWebsite = findViewById(R.id.btn_website);
btnWebsite.setOnClickListener(v -> openUrl(WEBSITE_BEEMDEVELOPMENT));
View btnRate = findViewById(R.id.btn_rate);
btnRate.setOnClickListener(v -> openUrl(PLAYSTORE_BEEMDEVELOPMENT ));
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create()
.setTheme(_themeHelper.getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.about_scroll_view), (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
0,
0,
0,
insets.bottom
);
return WindowInsetsCompat.CONSUMED;
});
}
private static String getCurrentAppVersion() {
if (BuildConfig.DEBUG) {
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
}
return BuildConfig.VERSION_NAME;
}
private void openUrl(String url) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW);
browserIntent.setData(Uri.parse(url));
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(browserIntent);
}
private void copyToClipboard(String text, @StringRes int messageId) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData data = ClipData.newPlainText("text/plain", text);
clipboard.setPrimaryClip(data);
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
}
private void openMail(String mailaddress) {
Intent mailIntent = new Intent(Intent.ACTION_SENDTO);
mailIntent.setData(Uri.parse("mailto:" + mailaddress));
mailIntent.putExtra(Intent.EXTRA_EMAIL, mailaddress);
mailIntent.putExtra(Intent.EXTRA_SUBJECT, R.string.app_name_full);
startActivity(Intent.createChooser(mailIntent, getString(R.string.email)));
}
private String getThemeColorAsHex(@AttrRes int attributeId) {
int color = MaterialColors.getColor(this, attributeId, getClass().getCanonicalName());
return String.format("%06X", 0xFFFFFF & color);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/AegisActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.core.view.ViewPropertyAnimatorCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.color.MaterialColors;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import javax.inject.Inject;
import dagger.hilt.InstallIn;
import dagger.hilt.android.AndroidEntryPoint;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
@AndroidEntryPoint
public abstract class AegisActivity extends AppCompatActivity implements VaultManager.LockListener {
protected Preferences _prefs;
protected ThemeHelper _themeHelper;
@Inject
protected VaultManager _vaultManager;
@Inject
protected AuditLogRepository _auditLogRepository;
@Inject
protected IconPackManager _iconPackManager;
private ActionModeStatusGuardHack _statusGuardHack;
@Override
protected void onCreate(Bundle savedInstanceState) {
// set the theme and locale before creating the activity
_prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
_themeHelper = new ThemeHelper(this, _prefs);
onSetTheme();
setLocale(_prefs.getLocale());
super.onCreate(savedInstanceState);
_statusGuardHack = new ActionModeStatusGuardHack();
// set FLAG_SECURE on the window of every AegisActivity
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// register a callback to listen for lock events
_vaultManager.registerLockListener(this);
}
@Override
@CallSuper
protected void onDestroy() {
_vaultManager.unregisterLockListener(this);
super.onDestroy();
}
@CallSuper
@Override
protected void onResume() {
super.onResume();
_vaultManager.setBlockAutoLock(false);
}
@SuppressLint("SoonBlockedPrivateApi")
@SuppressWarnings("JavaReflectionMemberAccess")
@Override
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
try {
// Call a private overload of the finish() method to prevent the app
// from disappearing from the recent apps menu
Method method = Activity.class.getDeclaredMethod("finish", int.class);
method.setAccessible(true);
method.invoke(this, 2); // FINISH_TASK_WITH_ACTIVITY = 2
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
// On recent Android versions, the overload of the finish() method
// used above is no longer accessible
finishAndRemoveTask();
}
}
/**
* Called when the activity is expected to set its theme.
*/
protected void onSetTheme() {
_themeHelper.setTheme(ThemeMap.DEFAULT);
}
protected void setLocale(Locale locale) {
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
protected boolean saveVault() {
try {
_vaultManager.save();
return true;
} catch (VaultRepositoryException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
return false;
}
}
protected boolean saveAndBackupVault() {
try {
_vaultManager.saveAndBackup();
return true;
} catch (VaultRepositoryException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
return false;
}
}
/**
* Closes this activity if it has become an orphan (isOrphan() == true) and launches MainActivity.
* @param savedInstanceState the bundle passed to onCreate.
* @return whether to abort onCreate.
*/
protected boolean abortIfOrphan(Bundle savedInstanceState) {
if (savedInstanceState == null || !isOrphan()) {
return false;
}
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return true;
}
@Override
public void onSupportActionModeStarted(@NonNull ActionMode mode) {
super.onSupportActionModeStarted(mode);
_statusGuardHack.apply(View.VISIBLE);
}
@Override
public void onSupportActionModeFinished(@NonNull ActionMode mode) {
super.onSupportActionModeFinished(mode);
_statusGuardHack.apply(View.GONE);
}
/**
* When starting/finishing an action mode, forcefully cancel the fade in/out animation and
* set the status bar color. This requires the abc_decor_view_status_guard colors to be set
* to transparent.
*
* This should fix any inconsistencies between the color of the action bar and the status bar
* when an action mode is active.
*/
private class ActionModeStatusGuardHack {
private Field _fadeAnimField;
private Field _actionModeViewField;
private Drawable _appBarBackground;
private ActionModeStatusGuardHack() {
try {
_fadeAnimField = getDelegate().getClass().getDeclaredField("mFadeAnim");
_fadeAnimField.setAccessible(true);
_actionModeViewField = getDelegate().getClass().getDeclaredField("mActionModeView");
_actionModeViewField.setAccessible(true);
} catch (NoSuchFieldException ignored) {
}
}
private void apply(int visibility) {
if (_fadeAnimField == null || _actionModeViewField == null) {
return;
}
ViewPropertyAnimatorCompat fadeAnim;
ViewGroup actionModeView;
try {
fadeAnim = (ViewPropertyAnimatorCompat) _fadeAnimField.get(getDelegate());
actionModeView = (ViewGroup) _actionModeViewField.get(getDelegate());
} catch (IllegalAccessException e) {
return;
}
AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
if (appBarLayout != null && _appBarBackground == null) {
_appBarBackground = appBarLayout.getBackground();
}
if (fadeAnim == null || actionModeView == null || appBarLayout == null || _appBarBackground == null) {
return;
}
fadeAnim.cancel();
if (visibility == View.VISIBLE) {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(1f);
int color = MaterialColors.getColor(appBarLayout, com.google.android.material.R.attr.colorSurfaceContainer);
appBarLayout.setBackgroundColor(color);
} else {
actionModeView.setVisibility(visibility);
actionModeView.setAlpha(0f);
appBarLayout.setBackground(_appBarBackground);
}
}
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was killed/locked by an external trigger while the Activity was still open.
*/
private boolean isOrphan() {
return !(this instanceof MainActivity)
&& !(this instanceof AuthActivity)
&& !(this instanceof IntroActivity)
&& !_vaultManager.isVaultLoaded();
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
public interface PrefEntryPoint {
Preferences getPreferences();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/AssignIconsActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.AssignIconEntry;
import com.beemdevelopment.aegis.ui.views.AssignIconAdapter;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public class AssignIconsActivity extends AegisActivity implements AssignIconAdapter.Listener {
private AssignIconAdapter _adapter;
private ArrayList _entries = new ArrayList<>();
private RecyclerView _entriesView;
private AssignIconsActivity.BackPressHandler _backPressHandler;
private ViewPreloadSizeProvider _preloadSizeProvider;
private IconPack _favoriteIconPack;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_assign_icons);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
ArrayList assignIconEntriesIds = (ArrayList) getIntent().getSerializableExtra("entries");
for (UUID entryId: assignIconEntriesIds) {
VaultEntry vaultEntry = _vaultManager.getVault().getEntryByUUID(entryId);
_entries.add(new AssignIconEntry(vaultEntry));
}
_backPressHandler = new AssignIconsActivity.BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
IconPreloadProvider modelProvider1 = new IconPreloadProvider();
EntryIconPreloadProvider modelProvider2 = new EntryIconPreloadProvider();
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
RecyclerViewPreloader preloader1 = new RecyclerViewPreloader(this, modelProvider1, _preloadSizeProvider, 10);
RecyclerViewPreloader preloader2 = new RecyclerViewPreloader(this, modelProvider2, _preloadSizeProvider, 10);
_adapter = new AssignIconAdapter(this);
_entriesView = findViewById(R.id.list_assign_icons);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
_entriesView.addItemDecoration(new SpacesItemDecoration(8));
_entriesView.addOnScrollListener(preloader1);
_entriesView.addOnScrollListener(preloader2);
Optional favoriteIconPack = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.findFirst();
if (!favoriteIconPack.isPresent()) {
throw new RuntimeException(String.format("Started %s without any icon packs present", AssignIconsActivity.class.getName()));
}
_favoriteIconPack = favoriteIconPack.get();
for (AssignIconEntry entry : _entries) {
IconPack.Icon suggestedIcon = findSuggestedIcon(entry);
if (suggestedIcon != null) {
entry.setNewIcon(suggestedIcon);
}
}
_adapter.addEntries(_entries);
}
private IconPack.Icon findSuggestedIcon(AssignIconEntry entry) {
List suggestedIcons = _favoriteIconPack.getSuggestedIcons(entry.getEntry().getIssuer());
if (suggestedIcons.size() > 0) {
return suggestedIcons.get(0);
}
return null;
}
private void saveAndFinish() throws IOException {
ArrayList uuids = new ArrayList<>();
for (AssignIconEntry selectedEntry : _entries) {
VaultEntry entry = selectedEntry.getEntry();
if (selectedEntry.getNewIcon() != null) {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(selectedEntry.getNewIcon().getFile())){
iconBytes = IOUtils.readFile(inStream);
}
VaultEntryIcon icon = new VaultEntryIcon(iconBytes, selectedEntry.getNewIcon().getIconType());
entry.setIcon(icon);
uuids.add(entry.getUUID());
_vaultManager.getVault().replaceEntry(entry);
}
}
Intent intent = new Intent();
intent.putExtra("entryUUIDs", uuids);
if (saveAndBackupVault()) {
setResult(RESULT_OK, intent);
finish();
}
}
private void discardAndFinish() {
Dialogs.showDiscardDialog(this,
(dialog, which) -> {
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
},
(dialog, which) -> finish());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_assign_icons, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
try {
saveAndFinish();
} catch (IOException e) {
Toast.makeText(this, R.string.saving_assign_icons_error, Toast.LENGTH_SHORT).show();
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onAssignIconEntryClick(AssignIconEntry entry) {
List iconPacks = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, entry.getEntry().getIssuer(), false, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
entry.setNewIcon(icon);
}
@Override
public void onCustomSelected() { }
});
Dialogs.showSecureDialog(dialog);
}
@Override
public void onSetPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
discardAndFinish();
}
}
private class EntryIconPreloadProvider implements ListPreloader.PreloadModelProvider {
@NonNull
@Override
public List getPreloadItems(int position) {
VaultEntry entry = _entries.get(position).getEntry();
if (entry.hasIcon()) {
return Collections.singletonList(entry);
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
RequestBuilder rb = Glide.with(AssignIconsActivity.this)
.load(entry.getIcon());
return GlideHelper.setCommonOptions(rb, entry.getIcon().getType());
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider {
@NonNull
@Override
public List getPreloadItems(int position) {
AssignIconEntry entry = _entries.get(position);
if (entry.getNewIcon() != null) {
return Collections.singletonList(entry.getNewIcon());
}
return Collections.emptyList();
}
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
RequestBuilder rb = Glide.with(AssignIconsActivity.this)
.load(icon.getFile());
return GlideHelper.setCommonOptions(rb, icon.getIconType());
}
}
private class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private final int _space;
public SpacesItemDecoration(int dpSpace) {
this._space = MetricsHelper.convertDpToPixels(AssignIconsActivity.this, dpSpace);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.left = _space;
outRect.right = _space;
outRect.bottom = _space;
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = _space;
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/AuthActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity {
// Permission request codes
private static final int CODE_PERM_NOTIFICATIONS = 0;
private EditText _textPassword;
private VaultFile _vaultFile;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
private Button _decryptButton;
private int _failedUnlockAttempts;
// the first time this activity is resumed after creation, it's possible to inhibit showing the
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
private boolean _inhibitBioPrompt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth);
TextInputLayout layoutStandard = findViewById(R.id.layout_standard);
TextInputLayout layoutNoAutofill = findViewById(R.id.layout_no_autofill);
EditText editStandard = findViewById(R.id.text_password);
EditText editNoAutofill = findViewById(R.id.text_password_no_autofill);
if (_prefs.isPinKeyboardEnabled()) {
layoutStandard.setVisibility(View.GONE);
layoutNoAutofill.setVisibility(View.VISIBLE);
_textPassword = editNoAutofill;
} else {
layoutStandard.setVisibility(View.VISIBLE);
layoutNoAutofill.setVisibility(View.GONE);
_textPassword = editStandard;
}
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
_decryptButton = findViewById(R.id.button_decrypt);
TextView biometricsButton = findViewById(R.id.button_biometrics);
getOnBackPressedDispatcher().addCallback(this, new BackPressHandler());
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
_decryptButton.performClick();
}
return false;
});
Intent intent = getIntent();
if (savedInstanceState == null) {
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
// A persistent notification is shown to let the user know that the vault is unlocked. Permission
// to do so is required since API 33, so for existing users, we have to request permission here
// in order to be able to show the notification after unlock.
//
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
PermissionHelper.request(this, CODE_PERM_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
}*/
} else {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
try {
_vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
getOnBackPressedDispatcher().onBackPressed();
});
return;
}
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
_slots = _vaultFile.getHeader().getSlots();
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
boolean invalidated = false;
try {
// find a biometric slot with an id that matches an alias in the keystore
for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) {
String id = slot.getUUID().toString();
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.containsKey(id)) {
SecretKey key = handle.getKey(id);
// if 'key' is null, it was permanently invalidated
if (key == null) {
invalidated = true;
continue;
}
_bioSlot = slot;
_bioKey = key;
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxBiometricInfo.setVisibility(View.VISIBLE);
biometricsButton.setVisibility(View.GONE);
}
}
_decryptButton.setOnClickListener(v -> {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
char[] password = EditTextHelper.getEditTextChars(_textPassword);
List slots = _slots.findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
_decryptButton.setEnabled(false);
});
biometricsButton.setOnClickListener(v -> {
if (_prefs.isPasswordReminderNeeded()) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(getString(R.string.password_reminder_dialog_title))
.setMessage(getString(R.string.password_reminder_dialog_message))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
showBiometricPrompt();
})
.create());
} else {
showBiometricPrompt();
}
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
private void selectPassword() {
_textPassword.selectAll();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
@Override
public void onResume() {
super.onResume();
boolean remindPassword = _prefs.isPasswordReminderNeeded();
if (_bioKey == null || remindPassword) {
focusPasswordField();
}
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
_bioPrompt = showBiometricPrompt();
}
_inhibitBioPrompt = false;
}
@Override
public void onPause() {
if (!isChangingConfigurations() && _bioPrompt != null) {
_bioPrompt.cancelAuthentication();
_bioPrompt = null;
}
super.onPause();
}
@Override
public void onAttachedToWindow() {
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
showPasswordReminder();
}
}
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
private void showPasswordReminder() {
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
popupLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setFocusable(false);
popup.setOutsideTouchable(true);
_textPassword.post(() -> {
if (isFinishing() || !_textPassword.isAttachedToWindow()) {
return;
}
// calculating the actual height of the popup window does not seem possible
// adding 25dp seems to look good enough
int yoff = _textPassword.getHeight()
+ popupLayout.getMeasuredHeight()
+ MetricsHelper.convertDpToPixels(this, 25);
popup.showAsDropDown(_textPassword, 0, -yoff);
});
_textPassword.postDelayed(popup::dismiss, 5000);
}
public BiometricPrompt showBiometricPrompt() {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0);
Cipher cipher;
try {
cipher = _bioSlot.createDecryptCipher(_bioKey);
} catch (SlotException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
return null;
}
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
BiometricPrompt prompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(false)
.build();
prompt.authenticate(info, cryptoObj);
return prompt;
}
private void finish(MasterKey key, boolean isSlotRepaired) {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
try {
_vaultManager.loadFrom(_vaultFile, creds);
if (isSlotRepaired) {
saveAndBackupVault();
}
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
}
setResult(RESULT_OK);
finish();
}
private void onInvalidPassword() {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(AuthActivity.this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
_failedUnlockAttempts ++;
if (_failedUnlockAttempts >= 3) {
_textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(true);
}
@Override
public void handleOnBackPressed() {
// This breaks predictive back gestures, but it doesn't make sense
// to go back to MainActivity when cancelling auth
setResult(RESULT_CANCELED);
finishAffinity();
}
}
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
}
if (result.getSlot().getType() == Slot.TYPE_PASSWORD) {
_prefs.resetPasswordReminderTimestamp();
}
finish(result.getKey(), result.isSlotRepaired());
} else {
_decryptButton.setEnabled(true);
_auditLogRepository.addVaultUnlockFailedPasswordEvent();
onInvalidPassword();
}
}
}
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
_bioPrompt = null;
if (!BiometricsHelper.isCanceled(errorCode)) {
_auditLogRepository.addVaultUnlockFailedBiometricsEvent();
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_bioPrompt = null;
MasterKey key;
BiometricSlot slot = _slots.find(BiometricSlot.class);
try {
key = slot.getKey(result.getCryptoObject().getCipher());
} catch (SlotException | SlotIntegrityException e) {
e.printStackTrace();
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
return;
}
finish(key, false);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
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.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.helpers.AnimationsHelper;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SafHelper;
import com.beemdevelopment.aegis.helpers.SimpleAnimationEndListener;
import com.beemdevelopment.aegis.helpers.SimpleTextWatcher;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.MotpInfo;
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.otp.YandexInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.GlideHelper;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
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.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public class EditEntryActivity extends AegisActivity {
private boolean _isNew = false;
private boolean _isManual = false;
private VaultEntry _origEntry;
private Collection _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private String _pickedMimeType;
private ShapeableImageView _iconView;
private ImageView _saveImageButton;
private TextInputEditText _textName;
private TextInputEditText _textIssuer;
private TextInputLayout _textGroupLayout;
private TextInputEditText _textGroup;
private TextInputEditText _textPeriodCounter;
private TextInputLayout _textPeriodCounterLayout;
private TextInputEditText _textDigits;
private TextInputLayout _textDigitsLayout;
private TextInputEditText _textSecret;
private TextInputEditText _textPin;
private LinearLayout _textPinLayout;
private TextInputEditText _textUsageCount;
private TextInputEditText _textNote;
private TextView _textLastUsed;
private AutoCompleteTextView _dropdownType;
private AutoCompleteTextView _dropdownAlgo;
private TextInputLayout _dropdownAlgoLayout;
private List _selectedGroups = new ArrayList<>();
private KropView _kropView;
private RelativeLayout _advancedSettingsHeader;
private LinearLayout _advancedSettingsLayout;
private BackPressHandler _backPressHandler;
private IconBackPressHandler _iconBackPressHandler;
private final ActivityResultLauncher pickImageResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
Intent data = activityResult.getData();
if (activityResult.getResultCode() != RESULT_OK || data == null || data.getData() == null) {
return;
}
_pickedMimeType = SafHelper.getMimeType(this, data.getData());
if (_pickedMimeType != null && _pickedMimeType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getError() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getError());
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_edit_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_groups = _vaultManager.getVault().getGroups();
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
}
_backPressHandler = new BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_iconBackPressHandler = new IconBackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _iconBackPressHandler);
// retrieve info from the calling activity
Intent intent = getIntent();
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
if (entryUUID != null) {
_origEntry = _vaultManager.getVault().getEntryByUUID(entryUUID);
} else {
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
_isManual = intent.getBooleanExtra("isManual", false);
_isNew = true;
setTitle(R.string.add_new_entry);
}
// set up fields
_iconView = findViewById(R.id.profile_drawable);
_kropView = findViewById(R.id.krop_view);
_saveImageButton = findViewById(R.id.iv_saveImage);
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textGroup = findViewById(R.id.text_group);
_textGroupLayout = findViewById(R.id.text_group_layout);
_textPeriodCounter = findViewById(R.id.text_period_counter);
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
_textDigits = findViewById(R.id.text_digits);
_textDigitsLayout = findViewById(R.id.text_digits_layout);
_textSecret = findViewById(R.id.text_secret);
_textPin = findViewById(R.id.text_pin);
_textPinLayout = findViewById(R.id.layout_pin);
_textUsageCount = findViewById(R.id.text_usage_count);
_textNote = findViewById(R.id.text_note);
_textLastUsed = findViewById(R.id.text_last_used);
_dropdownType = findViewById(R.id.dropdown_type);
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
_dropdownAlgo = findViewById(R.id.dropdown_algo);
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
if (!_isNew || !_isManual) {
int secretIndex = 0;
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
layoutBasic.removeView(layoutSecret);
if (!_isNew) {
secretIndex = 1;
layoutBasic.removeView(_textPinLayout);
layoutAdvanced.addView(_textPinLayout, 0);
((LinearLayout.LayoutParams) _textPinLayout.getLayoutParams()).topMargin = 0;
} else {
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
}
layoutAdvanced.addView(layoutSecret, secretIndex);
if (_isNew && !_isManual) {
setViewEnabled(layoutAdvanced, false);
}
} else {
LinearLayout layoutTypeAlgo = findViewById(R.id.layout_type_algo);
((LinearLayout.LayoutParams) layoutTypeAlgo.getLayoutParams()).topMargin = 0;
}
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
_advancedSettingsLayout = findViewById(R.id.layout_advanced);
// fill the fields with values if possible
GlideHelper.loadEntryIcon(Glide.with(this), _origEntry, _iconView);
if (_origEntry.hasIcon()) {
_hasCustomIcon = true;
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
_textNote.setText(_origEntry.getNote());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
} else if (info instanceof HotpInfo) {
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
String secretString = (info instanceof MotpInfo) ? Hex.encode(secretBytes) : Base32.encode(secretBytes);
_textSecret.setText(secretString);
}
_dropdownType.setText(_origEntry.getInfo().getType(), false);
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
if (info instanceof YandexInfo) {
_textPin.setText(((YandexInfo) info).getPin());
} else if (info instanceof MotpInfo) {
_textPin.setText(((MotpInfo) info).getPin());
}
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
Set groups = _origEntry.getGroups();
if (groups.isEmpty()) {
_textGroup.setText(getString(R.string.no_group));
} else {
String text = groups.stream().map(uuid -> {
VaultGroup group = _vaultManager.getVault().getGroupByUUID(uuid);
return group.getName();
})
.collect(Collectors.joining(", "));
_selectedGroups.addAll(groups);
_textGroup.setText(text);
}
// Update the icon if the issuer or name has changed
_textIssuer.addTextChangedListener(_nameChangeListener);
_textName.addTextChangedListener(_nameChangeListener);
// Register listeners to trigger validation
_textIssuer.addTextChangedListener(_validationListener);
_textGroup.addTextChangedListener(_validationListener);
_textName.addTextChangedListener(_validationListener);
_textNote.addTextChangedListener(_validationListener);
_textSecret.addTextChangedListener(_validationListener);
_dropdownType.addTextChangedListener(_validationListener);
_dropdownAlgo.addTextChangedListener(_validationListener);
_textPeriodCounter.addTextChangedListener(_validationListener);
_textDigits.addTextChangedListener(_validationListener);
_textPin.addTextChangedListener(_validationListener);
// show/hide period and counter fields on type change
_dropdownType.setOnItemClickListener((parent, view, position, id) -> {
String type = _dropdownType.getText().toString().toLowerCase(Locale.ROOT);
switch (type) {
case SteamInfo.ID:
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(SteamInfo.DIGITS));
break;
case TotpInfo.ID:
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
case HotpInfo.ID:
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
case YandexInfo.ID:
_dropdownAlgo.setText(YandexInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(YandexInfo.DIGITS));
break;
case MotpInfo.ID:
_dropdownAlgo.setText(MotpInfo.ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(MotpInfo.PERIOD));
_textDigits.setText(String.valueOf(MotpInfo.DIGITS));
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
updateAdvancedFieldStatus(type);
updatePinFieldVisibility(type);
});
_iconView.setOnClickListener(v -> {
startIconSelection();
});
_textGroup.setShowSoftInputOnFocus(false);
_textGroup.setOnClickListener(v -> showGroupSelectionDialog());
_textGroup.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
showGroupSelectionDialog();
}
});
_textGroupLayout.setOnClickListener(v -> {
showGroupSelectionDialog();
});
_textUsageCount.setText(_prefs.getUsageCount(entryUUID).toString());
setLastUsedTimestamp(_prefs.getLastUsedTimestamp(entryUUID));
}
private void showGroupSelectionDialog() {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.dialog_select_groups, null);
dialog.setContentView(view);
ChipGroup chipGroup = view.findViewById(R.id.groupChipGroup);
TextView addGroupInfo = view.findViewById(R.id.addGroupInfo);
LinearLayout addGroup = view.findViewById(R.id.addGroup);
Button clearButton = view.findViewById(R.id.btnClear);
Button saveButton = view.findViewById(R.id.btnSave);
chipGroup.removeAllViews();
addGroupInfo.setVisibility(View.VISIBLE);
addGroup.setVisibility(View.VISIBLE);
for (VaultGroup group : _groups) {
addChipTo(chipGroup, new VaultGroupModel(group), false);
}
addGroup.setOnClickListener(v1 -> {
Dialogs.TextInputListener onAddGroup = text -> {
String groupName = new String(text).trim();
if (!groupName.isEmpty()) {
VaultGroup group = _vaultManager.getVault().findGroupByName(groupName);
if (group == null) {
group = new VaultGroup(groupName);
_vaultManager.getVault().addGroup(group);
}
_selectedGroups.add(group.getUUID());
addChipTo(chipGroup, new VaultGroupModel(group), true);
}
};
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, onAddGroup);
});
saveButton.setOnClickListener(v1 -> {
if(getCheckedUUID(chipGroup).isEmpty()) {
_selectedGroups.clear();
_textGroup.setText(getString(R.string.no_group));
} else {
_selectedGroups.clear();
_selectedGroups.addAll(getCheckedUUID(chipGroup));
_textGroup.setText(getCheckedNames(chipGroup));
}
dialog.dismiss();
});
clearButton.setOnClickListener(v1 -> {
chipGroup.clearCheck();
});
Dialogs.showSecureDialog(dialog);
}
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group, Boolean isNew) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setChecked((!_selectedGroups.isEmpty() && _selectedGroups.contains(group.getUUID())) || isNew);
chip.setCheckedIconVisible(true);
chip.setTag(group);
chipGroup.addView(chip);
}
private static Set getCheckedUUID(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
})
.collect(Collectors.toSet());
}
private static String getCheckedNames(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.map(i -> {
Chip chip = chipGroup.findViewById(i);
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getName();
})
.collect(Collectors.joining(", "));
}
private void updateAdvancedFieldStatus(String otpType) {
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID)
&& !otpType.equals(MotpInfo.ID) && (!_isNew || _isManual);
_textDigitsLayout.setEnabled(enabled);
_textPeriodCounterLayout.setEnabled(enabled);
_dropdownAlgoLayout.setEnabled(enabled);
}
private void updatePinFieldVisibility(String otpType) {
boolean visible = otpType.equals(YandexInfo.ID) || otpType.equals(MotpInfo.ID);
_textPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration((long) (220 * AnimationsHelper.Scale.ANIMATOR.getValue(this)));
_advancedSettingsHeader.startAnimation(fadeOut);
fadeOut.setAnimationListener(new SimpleAnimationEndListener((a) -> {
_advancedSettingsHeader.setVisibility(View.GONE);
_advancedSettingsLayout.setVisibility(View.VISIBLE);
_advancedSettingsLayout.animate()
.setInterpolator(new AccelerateInterpolator())
.setDuration((long) (250 * AnimationsHelper.Scale.ANIMATOR.getValue(this)))
.alpha(1);
}));
}
private boolean hasUnsavedChanges(VaultEntry newEntry) {
return _hasChangedIcon || !_origEntry.equals(newEntry);
}
private void discardAndFinish() {
AtomicReference msg = new AtomicReference<>();
AtomicReference entry = new AtomicReference<>();
try {
entry.set(parseEntry());
} catch (ParseException e) {
msg.set(e.getMessage());
}
if (!hasUnsavedChanges(entry.get())) {
finish();
return;
}
// ask for confirmation if the entry has been changed
Dialogs.showDiscardDialog(EditEntryActivity.this,
(dialog, which) -> {
// if the entry couldn't be parsed, we show an error dialog
if (msg.get() != null) {
onSaveError(msg.get());
return;
}
addAndFinish(entry.get());
},
(dialog, which) -> finish()
);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
onSave();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
} else if (itemId == R.id.action_edit_icon) {
startIconSelection();
} else if (itemId == R.id.action_reset_usage_count) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.action_reset_usage_count)
.setMessage(R.string.action_reset_usage_count_dialog)
.setPositiveButton(android.R.string.yes, (dialog, which) -> resetUsageCount())
.setNegativeButton(android.R.string.no, null)
.create());
} else if (itemId == R.id.action_default_icon) {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
private void startImageSelectionActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
fileIntent.setType("image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
_vaultManager.fireIntentLauncher(this, chooserIntent, pickImageResultLauncher);
}
private void resetUsageCount() {
_prefs.resetUsageCount(_origEntry.getUUID());
_textUsageCount.setText("0");
}
private void startIconSelection() {
List iconPacks = _iconPackManager.getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
if (iconPacks.size() == 0) {
startImageSelectionActivity();
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), true, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);
}
@Override
public void onCustomSelected() {
startImageSelectionActivity();
}
});
Dialogs.showSecureDialog(dialog);
}
private void selectIcon(IconPack.Icon icon) {
_selectedIcon = icon;
_hasCustomIcon = true;
_hasChangedIcon = true;
GlideHelper.loadIcon(Glide.with(EditEntryActivity.this), icon, _iconView);
}
private void startEditingIcon(Uri data) {
Glide.with(this)
.asBitmap()
.load(data)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(v -> {
stopEditingIcon(true);
});
_iconBackPressHandler.setEnabled(true);
}
private void stopEditingIcon(boolean save) {
if (save && _selectedIcon == null) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
}
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save;
_iconBackPressHandler.setEnabled(false);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit, menu);
if (_isNew) {
menu.findItem(R.id.action_delete).setVisible(false);
}
if (!_hasCustomIcon) {
menu.findItem(R.id.action_default_icon).setVisible(false);
}
return true;
}
private void addAndFinish(VaultEntry entry) {
// It's possible that the new entry was already added to the vault, but writing the
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
// again would cause a crash in that case, so the isEntryDuplicate check prevents
// that.
VaultRepository vault = _vaultManager.getVault();
if (_isNew && !vault.isEntryDuplicate(entry)) {
vault.addEntry(entry);
} else {
vault.replaceEntry(entry);
}
saveAndFinish(entry, false);
}
private void setLastUsedTimestamp(long timestamp) {
String readableDate = getString(R.string.last_used_never);
if (timestamp != 0) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.getDefault());
readableDate = dateFormat.format(new Date(timestamp));
}
_textLastUsed.setText(String.format("%s: %s", getString(R.string.last_used), readableDate));
}
private void deleteAndFinish(VaultEntry entry) {
_vaultManager.getVault().removeEntry(entry);
saveAndFinish(entry, true);
}
private void saveAndFinish(VaultEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete);
if (saveAndBackupVault()) {
setResult(RESULT_OK, intent);
finish();
}
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
}
private VaultEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
}
String type = _dropdownType.getText().toString();
String algo = _dropdownAlgo.getText().toString();
String lowerCasedType = type.toLowerCase(Locale.ROOT);
if (lowerCasedType.equals(YandexInfo.ID) || lowerCasedType.equals(MotpInfo.ID)) {
int pinLength = _textPin.length();
if (pinLength < 4) {
throw new ParseException("PIN is a required field. Must have a minimum length of 4 digits.");
}
if (pinLength != 4 && lowerCasedType.equals(MotpInfo.ID)) {
throw new ParseException("PIN must have a length of 4 digits.");
}
}
int digits;
try {
digits = Integer.parseInt(_textDigits.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Digits is not an integer.");
}
byte[] secret;
try {
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
secret = (lowerCasedType.equals(MotpInfo.ID)) ?
Hex.decode(secretString) : GoogleAuthInfo.parseSecret(secretString);
if (secret.length == 0) {
throw new ParseException("Secret cannot be empty");
}
} catch (EncodingException e) {
String exceptionMessage = (lowerCasedType.equals(MotpInfo.ID)) ?
"Secret is not valid hexadecimal" : "Secret is not valid base32.";
throw new ParseException(exceptionMessage);
}
OtpInfo info;
try {
switch (type.toLowerCase(Locale.ROOT)) {
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, parsePeriod());
break;
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, parsePeriod());
break;
case HotpInfo.ID:
long counter;
try {
counter = Long.parseLong(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Counter is not an integer.");
}
info = new HotpInfo(secret, algo, digits, counter);
break;
case YandexInfo.ID:
info = new YandexInfo(secret, _textPin.getText().toString());
break;
case MotpInfo.ID:
info = new MotpInfo(secret, _textPin.getText().toString());
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
info.setDigits(digits);
info.setAlgorithm(algo);
} catch (OtpInfoException e) {
throw new ParseException("The entered info is incorrect: " + e.getMessage());
}
VaultEntry entry = Cloner.clone(_origEntry);
entry.setInfo(info);
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
entry.setNote(_textNote.getText().toString());
if (_selectedGroups.isEmpty()) {
entry.setGroups(new HashSet<>());
} else {
entry.setGroups(new HashSet<>(_selectedGroups));
}
if (_hasChangedIcon) {
if (_hasCustomIcon) {
VaultEntryIcon icon;
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
IconType iconType = _pickedMimeType == null
? IconType.INVALID : IconType.fromMimeType(_pickedMimeType);
if (iconType == IconType.INVALID) {
iconType = bitmap.hasAlpha() ? IconType.PNG : IconType.JPEG;
}
icon = BitmapHelper.toVaultEntryIcon(bitmap, iconType);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
iconBytes = IOUtils.readFile(inStream);
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
icon = new VaultEntryIcon(iconBytes, _selectedIcon.getIconType());
}
entry.setIcon(icon);
} else {
entry.setIcon(null);
}
}
return entry;
}
private void onSaveError(String msg) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(getString(R.string.saving_profile_error))
.setMessage(msg)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, null)
.create());
}
private boolean onSave() {
if (_iconBackPressHandler.isEnabled()) {
stopEditingIcon(true);
}
VaultEntry entry;
try {
entry = parseEntry();
} catch (ParseException e) {
onSaveError(e.getMessage());
return false;
}
if (_isNew) {
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (entry.hasSameNameAndIssuer(existing)) {
showDuplicateBottomSheet(entry);
return false;
}
}
}
addAndFinish(entry);
return true;
}
private void showDuplicateBottomSheet(VaultEntry newEntry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
View view = getLayoutInflater().inflate(R.layout.dialog_duplicate_entry, null);
dialog.setContentView(view);
dialog.setCancelable(false);
View overwrite = view.findViewById(R.id.overwrite_entry);
View addSuffix = view.findViewById(R.id.create_new_entry);
View cancel = view.findViewById(R.id.cancel_save);
TextView suffixSubtext = view.findViewById(R.id.duplicate_suffix_subtitle);
String baseName = newEntry.getName();
Set existingNames = new HashSet<>();
for (VaultEntry e : _vaultManager.getVault().getEntries()) {
if (e.getIssuer().equals(newEntry.getIssuer())) {
existingNames.add(e.getName());
}
}
int counter = 2;
String newName;
do {
newName = baseName + " #" + counter++;
} while (existingNames.contains(newName));
suffixSubtext.setText(getString(R.string.dialog_duplicate_entry_suffix_subtitle, newName));
overwrite.setOnClickListener(v -> {
List duplicates = new ArrayList<>();
for (VaultEntry existing : _vaultManager.getVault().getEntries()) {
if (existing.hasSameNameAndIssuer(newEntry)) {
duplicates.add(existing);
}
}
Resources res = getResources();
String message = res.getQuantityString(
R.plurals.dialog_duplicate_entry_overwrite_dialog_message,
duplicates.size(),
duplicates.size(),
newEntry.getIssuer(),
newEntry.getName()
);
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_duplicate_entry_overwrite_dialog_title)
.setMessage(message)
.setPositiveButton(R.string.action_delete, (d, which) -> {
for (VaultEntry dup : duplicates) {
_vaultManager.getVault().removeEntry(dup);
}
dialog.dismiss();
addAndFinish(newEntry);
})
.setNegativeButton(android.R.string.no, null)
.show();
});
String finalNewName = newName;
addSuffix.setOnClickListener(v -> {
newEntry.setName(finalNewName);
dialog.dismiss();
addAndFinish(newEntry);
});
cancel.setOnClickListener(v -> dialog.dismiss());
Dialogs.showSecureDialog(dialog);
}
private static void setViewEnabled(View view, boolean enabled) {
view.setEnabled(enabled);
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
setViewEnabled(group.getChildAt(i), enabled);
}
}
}
private final TextWatcher _validationListener = new SimpleTextWatcher((s) -> {
updateBackPressHandlerState();
});
private final TextWatcher _nameChangeListener = new SimpleTextWatcher((s) -> {
if (!_hasCustomIcon) {
TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString(), _iconView);
_iconView.setImageDrawable(drawable);
}
});
private void updateBackPressHandlerState() {
VaultEntry entry = null;
try {
entry = parseEntry();
} catch (ParseException ignored) {
}
boolean backEnabled = hasUnsavedChanges(entry);
_backPressHandler.setEnabled(backEnabled);
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
discardAndFinish();
}
}
private class IconBackPressHandler extends OnBackPressedCallback {
public IconBackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
stopEditingIcon(false);
}
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
private static class CustomSvgIcon extends IconPack.Icon {
private final File _file;
protected CustomSvgIcon(File file) {
super(file.getAbsolutePath(), null, null, null);
_file = file;
}
@Nullable
@Override
public File getFile() {
return _file;
}
@Override
public IconType getIconType() {
return IconType.SVG;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/ExitActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class ExitActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
}
public static void exitAppAndRemoveFromRecents(Context context) {
Intent intent = new Intent(context, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_NO_ANIMATION |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/GroupManagerActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private HashSet _removedGroups;
private RecyclerView _groupsView;
private View _emptyStateView;
private BackPressHandler _backPressHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_groups);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
_backPressHandler = new BackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _backPressHandler);
_removedGroups = new HashSet<>();
if (savedInstanceState != null) {
List removedGroups = savedInstanceState.getStringArrayList("removedGroups");
if (removedGroups != null) {
for (String uuid : removedGroups) {
_removedGroups.add(UUID.fromString(uuid));
}
}
}
ItemTouchHelper touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(
@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
int draggedItemIndex = viewHolder.getBindingAdapterPosition();
int targetIndex = target.getBindingAdapterPosition();
_adapter.onItemMove(draggedItemIndex, targetIndex);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { }
});
_adapter = new GroupAdapter(this);
_groupsView = findViewById(R.id.list_groups);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_groupsView.setLayoutManager(layoutManager);
_groupsView.setAdapter(_adapter);
_groupsView.setNestedScrollingEnabled(false);
touchHelper.attachToRecyclerView(_groupsView);
for (VaultGroup group : _vaultManager.getVault().getGroups()) {
if (!_removedGroups.contains(group.getUUID())) {
_adapter.addGroup(group);
}
}
_emptyStateView = findViewById(R.id.vEmptyList);
updateEmptyState();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
ArrayList removed = new ArrayList<>();
for (UUID uuid : _removedGroups) {
removed.add(uuid.toString());
}
outState.putStringArrayList("removedGroups", removed);
}
@Override
public void onEditGroup(VaultGroup group) {
Dialogs.TextInputListener onEditGroup = text -> {
String newGroupName = new String(text).trim();
if (!newGroupName.isEmpty()) {
VaultGroup newGroup = Cloner.clone(group);
newGroup.setName(newGroupName);
_adapter.replaceGroup(group.getUUID(), newGroup);
_backPressHandler.setEnabled(true);
}
};
Dialogs.showTextInputDialog(GroupManagerActivity.this, R.string.rename_group, R.string.group_name_hint, onEditGroup, group.getName());
}
@Override
public void onRemoveGroup(VaultGroup group) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.remove_group)
.setMessage(R.string.remove_group_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
_backPressHandler.setEnabled(true);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
public void onRemoveUnusedGroups() {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.remove_unused_groups)
.setMessage(R.string.remove_unused_groups_description)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
Set unusedGroups = new HashSet<>(_vaultManager.getVault().getGroups());
unusedGroups.removeAll(_vaultManager.getVault().getUsedGroups());
for (VaultGroup group : unusedGroups) {
_removedGroups.add(group.getUUID());
_adapter.removeGroup(group);
}
_backPressHandler.setEnabled(true);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
private void saveAndFinish() {
if (!_removedGroups.isEmpty()) {
for (UUID uuid : _removedGroups) {
_vaultManager.getVault().removeGroup(uuid);
}
}
_vaultManager.getVault().replaceGroups(_adapter.getGroups());
saveAndBackupVault();
finish();
}
private void discardAndFinish() {
if (_removedGroups.isEmpty()) {
finish();
return;
}
Dialogs.showDiscardDialog(this,
(dialog, which) -> saveAndFinish(),
(dialog, which) -> finish());
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_groups, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
discardAndFinish();
} else if (itemId == R.id.action_save) {
saveAndFinish();
} else if (itemId == R.id.action_delete_unused_groups) {
onRemoveUnusedGroups();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_groupsView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_groupsView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
private class BackPressHandler extends OnBackPressedCallback {
public BackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
discardAndFinish();
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/ImportEntriesActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.RootShellTask;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ImportEntriesActivity extends AegisActivity {
private View _view;
private Menu _menu;
private RecyclerView _entriesView;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
private UUIDMap _importedGroups;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_view = findViewById(R.id.importEntriesRootView);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_outline_close_24);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
_entriesView = findViewById(R.id.list_entries);
_entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_fabScrollHelper.onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
_entriesView.setLayoutManager(layoutManager);
_entriesView.setAdapter(_adapter);
_entriesView.setNestedScrollingEnabled(false);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
if (_vaultManager.getVault().getEntries().size() > 0
&& _menu.findItem(R.id.toggle_wipe_vault).isChecked()) {
showWipeEntriesDialog();
} else {
saveAndFinish(false);
}
});
_fabScrollHelper = new FabScrollHelper(fab);
DatabaseImporter.Definition importerDef = (DatabaseImporter.Definition) getIntent().getSerializableExtra("importerDef");
startImport(importerDef, (File) getIntent().getSerializableExtra("file"));
}
private void startImport(DatabaseImporter.Definition importerDef, @Nullable File file) {
DatabaseImporter importer = DatabaseImporter.create(this, importerDef.getType());
if (file == null) {
if (importer.isInstalledAppVersionSupported()) {
startImportApp(importer);
} else {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.warning)
.setMessage(getString(R.string.app_version_error, importerDef.getName()))
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.yes, (dialog1, which) -> {
startImportApp(importer);
})
.setNegativeButton(R.string.no, (dialog1, which) -> {
finish();
})
.create());
}
} else {
startImportFile(importer, file);
}
}
private void startImportFile(@NonNull DatabaseImporter importer, @NonNull File file) {
try (InputStream stream = new FileInputStream(file)) {
DatabaseImporter.State state = importer.read(stream);
processImporterState(state);
} catch (FileNotFoundException e) {
Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (DatabaseImporterException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
}
}
private void startImportApp(@NonNull DatabaseImporter importer) {
RootShellTask task = new RootShellTask(this, shell -> {
if (isFinishing()) {
return;
}
if (shell == null || !shell.isRoot()) {
Toast.makeText(this, R.string.root_error, Toast.LENGTH_SHORT).show();
finish();
return;
}
try {
DatabaseImporter.State state = importer.readFromApp(shell);
processImporterState(state);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(this, R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
finish();
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
} finally {
try {
shell.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
task.execute(this);
}
private void processImporterState(DatabaseImporter.State state) {
try {
if (state.isEncrypted()) {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
processDecryptedImporterState(state);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(ImportEntriesActivity.this, R.string.decryption_error, e, (dialog, which) -> finish());
}
@Override
public void onCanceled() {
finish();
}
});
} else {
processDecryptedImporterState(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
}
}
private void processDecryptedImporterState(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
return;
}
Map icons = result.getEntries().getValues().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!icons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, newIcons -> {
for (Map.Entry mapEntry : newIcons.entrySet()) {
VaultEntry entry = result.getEntries().getByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}
processImporterResult(result);
});
task.execute(getLifecycle(), icons);
} else {
processImporterResult(result);
}
}
private void processImporterResult(DatabaseImporter.Result result) {
List importEntries = new ArrayList<>();
for (VaultEntry entry : result.getEntries().getValues()) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
importEntries.add(importEntry);
}
_importedGroups = result.getGroups();
List errors = result.getErrors();
if (errors.size() > 0) {
String message = getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), errors.size());
Dialogs.showMultiExceptionDialog(this, R.string.import_error_title, message, errors, null);
}
findDuplicates(importEntries);
}
private void showWipeEntriesDialog() {
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
R.string.dialog_wipe_entries_message,
R.string.dialog_wipe_entries_checkbox,
this::saveAndFinish
);
}
private void saveAndFinish(boolean wipeEntries) {
VaultRepository vault = _vaultManager.getVault();
if (wipeEntries) {
vault.wipeContents();
}
// Given the list of selected entries, collect the UUID's of all groups
// that we're actually going to import
List selectedEntries = _adapter.getCheckedEntries();
List selectedGroupUuids = new ArrayList<>();
for (ImportEntry entry : selectedEntries) {
selectedGroupUuids.addAll(entry.getEntry().getGroups());
}
// Add all of the new groups to the vault. If a group with the same name already
// exists in the vault, rewrite all entries in that group to reference the existing group.
for (VaultGroup importedGroup : _importedGroups) {
if (!selectedGroupUuids.contains(importedGroup.getUUID())) {
continue;
}
VaultGroup existingGroup = vault.findGroupByUUID(importedGroup.getUUID());
if (existingGroup != null) {
continue;
}
existingGroup = vault.findGroupByName(importedGroup.getName());
if (existingGroup == null) {
vault.addGroup(importedGroup);
} else {
for (ImportEntry entry : selectedEntries) {
Set entryGroups = entry.getEntry().getGroups();
if (entryGroups.contains(importedGroup.getUUID())) {
entryGroups.remove(importedGroup.getUUID());
entryGroups.add(existingGroup.getUUID());
}
}
}
}
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry entry = selectedEntry.getEntry();
// temporary: randomize the UUID of duplicate entries and add them anyway
if (vault.isEntryDuplicate(entry)) {
entry.resetUUID();
}
vault.addEntry(entry);
}
if (saveAndBackupVault()) {
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK, null);
if (_iconPackManager.hasIconPack()) {
ArrayList assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (ImportEntry entry : selectedEntries) {
assignIconEntriesIds.add(entry.getEntry().getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.import_assign_icons_dialog_title)
.setMessage(R.string.import_assign_icons_dialog_text)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
startActivity(assignIconIntent);
finish();
})
.setNegativeButton(android.R.string.no, ((dialogInterface, i) -> finish()))
.create());
} else {
finish();
}
}
}
private void findDuplicates(List importEntries) {
List duplicateEntries = new ArrayList<>();
for (ImportEntry importEntry: importEntries) {
boolean exists = _vaultManager.getVault().getEntries().stream().anyMatch(item ->
item.getIssuer().equals(importEntry.getEntry().getIssuer()) &&
Arrays.equals(item.getInfo().getSecret(), importEntry.getEntry().getInfo().getSecret()));
if (exists) {
duplicateEntries.add(importEntry.getEntry().getUUID());
}
}
if (duplicateEntries.size() == 0) {
return;
}
_adapter.setCheckboxStates(duplicateEntries, false);
Snackbar snackbar = Snackbar.make(_view, getResources().getQuantityString(R.plurals.import_duplicate_toast, duplicateEntries.size(), duplicateEntries.size()), Snackbar.LENGTH_INDEFINITE);
snackbar.addCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar sb) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() + snackbarHeight * 2
);
}
@Override
public void onDismissed(Snackbar sb, int event) {
int snackbarHeight = sb.getView().getHeight();
_entriesView.setPadding(
_entriesView.getPaddingLeft(),
_entriesView.getPaddingTop(),
_entriesView.getPaddingRight(),
_entriesView.getPaddingBottom() - snackbarHeight * 2
);
}
});
snackbar.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
_adapter.setCheckboxStates(duplicateEntries, true);
}
});
snackbar.show();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_import_entries, _menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
} else if (itemId == R.id.toggle_checkboxes) {
_adapter.toggleCheckboxes();
} else if (itemId == R.id.toggle_wipe_vault) {
item.setChecked(!item.isChecked());
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/IntroActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
import android.os.Bundle;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
public class IntroActivity extends IntroBaseActivity {
// Permission request codes
private static final int CODE_PERM_NOTIFICATIONS = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addSlide(WelcomeSlide.class);
addSlide(SecurityPickerSlide.class);
addSlide(SecuritySetupSlide.class);
addSlide(DoneSlide.class);
}
@Override
protected boolean onBeforeSlideChanged(Class extends SlideFragment> oldSlide, @NonNull Class extends SlideFragment> newSlide) {
// hide the keyboard before every slide change
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(findViewById(android.R.id.content).getWindowToken(), 0);
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
skipToSlide(DoneSlide.class);
return true;
}
if (oldSlide == WelcomeSlide.class
&& newSlide == SecurityPickerSlide.class
&& getState().getBoolean("imported")) {
skipToSlide(DoneSlide.class);
return true;
}
// on the welcome page, we don't want the keyboard to push any views up
getWindow().setSoftInputMode(newSlide == WelcomeSlide.class
? WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
: WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
return false;
}
@Override
protected void onAfterSlideChanged(@Nullable Class extends SlideFragment> oldSlide, @NonNull Class extends SlideFragment> newSlide) {
// If the user has enabled encryption, we need to request permission to show notifications
// in order to be able to show the "Vault unlocked" notification.
//
// NOTE: Disabled for now. See issue: #1047
/*if (newSlide == DoneSlide.class && getState().getSerializable("creds") != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
PermissionHelper.request(this, CODE_PERM_NOTIFICATIONS, Manifest.permission.POST_NOTIFICATIONS);
}
}*/
}
@Override
protected void onDonePressed() {
Bundle state = getState();
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
if (!state.getBoolean("imported")) {
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
if (cryptType == CRYPT_TYPE_INVALID
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
}
try {
_vaultManager.initNew(creds);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
return;
}
} else {
VaultFile vaultFile;
try {
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
return;
}
try {
_vaultManager.loadFrom(vaultFile, creds);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e);
return;
}
}
// skip the intro from now on
_prefs.setIntroDone(true);
setResult(RESULT_OK);
finish();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/LicensesActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.mikepenz.aboutlibraries.LibsBuilder;
import com.mikepenz.aboutlibraries.ui.LibsActivity;
import org.jetbrains.annotations.Nullable;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
public class LicensesActivity extends LibsActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
LibsBuilder builder = new LibsBuilder()
.withSearchEnabled(true)
.withAboutMinimalDesign(true)
.withActivityTitle(getString(R.string.title_activity_licenses));
setIntent(builder.intent(this));
Preferences _prefs = EarlyEntryPoints.get(getApplicationContext(), PrefEntryPoint.class).getPreferences();
ThemeHelper themeHelper = new ThemeHelper(this, _prefs);
themeHelper.setTheme(ThemeMap.DEFAULT);
super.onCreate(savedInstanceState);
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
public interface PrefEntryPoint {
Preferences getPreferences();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.provider.Settings;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.beemdevelopment.aegis.GroupPlaceholderType;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.FabMenuHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.ui.models.ErrorCardInfo;
import com.beemdevelopment.aegis.ui.models.VaultGroupModel;
import com.beemdevelopment.aegis.ui.tasks.IconOptimizationTask;
import com.beemdevelopment.aegis.ui.tasks.QrDecodeTask;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.util.ClipboardUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// Permission request codes
private static final int CODE_PERM_CAMERA = 0;
private boolean _loaded;
private boolean _isRecreated;
private boolean _isDPadPressed;
private boolean _isDoingIntro;
private boolean _isAuthenticating;
private String _submittedSearchQuery;
private String _pendingSearchQuery;
private List _selectedEntries;
private Menu _menu;
private SearchView _searchView;
private EntryListView _entryListView;
private Collection _groups;
private ChipGroup _groupChip;
private Set _groupFilter;
private Set _prefGroupFilter;
private FabScrollHelper _fabScrollHelper;
private FabMenuHelper _fabMenuHelper;
private ActionMode _actionMode;
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
private LockBackPressHandler _lockBackPressHandler;
private SearchViewBackPressHandler _searchViewBackPressHandler;
private ActionModeBackPressHandler _actionModeBackPressHandler;
private FabMenuBackPressHandler _fabMenuBackPressHandler;
private final ActivityResultLauncher authResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
_isAuthenticating = false;
if (activityResult.getResultCode() == RESULT_OK) {
onDecryptResult();
}
});
private final ActivityResultLauncher introResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
_isDoingIntro = false;
if (activityResult.getResultCode() == RESULT_OK) {
onIntroResult();
}
});
private final ActivityResultLauncher scanResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onScanResult(activityResult.getData());
});
private final ActivityResultLauncher assignIconsResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onAssignIconsResult();
});
private final ActivityResultLauncher preferenceResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> onPreferencesResult());
private final ActivityResultLauncher editEntryResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onEditEntryResult();
});
private final ActivityResultLauncher addEntryResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() != RESULT_OK || activityResult.getData() == null) {
return;
}
onAddEntryResult(activityResult.getData());
});
private final ActivityResultLauncher codeScanResultLauncher =
registerForActivityResult(new StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() == RESULT_OK && activityResult.getData() != null) {
onScanImageResult(activityResult.getData());
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_loaded = false;
_isDPadPressed = false;
_isDoingIntro = false;
_isAuthenticating = false;
if (savedInstanceState != null) {
_isRecreated = true;
_pendingSearchQuery = savedInstanceState.getString("pendingSearchQuery");
_submittedSearchQuery = savedInstanceState.getString("submittedSearchQuery");
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
_isAuthenticating = savedInstanceState.getBoolean("isAuthenticating");
}
_lockBackPressHandler = new LockBackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _lockBackPressHandler);
_searchViewBackPressHandler = new SearchViewBackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _searchViewBackPressHandler);
_actionModeBackPressHandler = new ActionModeBackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _actionModeBackPressHandler);
_fabMenuBackPressHandler = new FabMenuBackPressHandler();
getOnBackPressedDispatcher().addCallback(this, _fabMenuBackPressHandler);
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setCodeGroupSize(_prefs.getCodeGroupSize());
_entryListView.setAccountNamePosition(_prefs.getAccountNamePosition());
_entryListView.setShowIcon(_prefs.isIconVisible());
_entryListView.setShowExpirationState(_prefs.getShowExpirationState());
_entryListView.setShowNextCode(_prefs.getShowNextCode());
_entryListView.setOnlyShowNecessaryAccountNames(_prefs.onlyShowNecessaryAccountNames());
_entryListView.setHighlightEntry(_prefs.isEntryHighlightEnabled());
_entryListView.setPauseFocused(_prefs.isPauseFocusedEnabled());
_entryListView.setTapToReveal(_prefs.isTapToRevealEnabled());
_entryListView.setTapToRevealTime(_prefs.getTapToRevealTime());
_entryListView.setViewMode(_prefs.getCurrentViewMode());
_entryListView.setSortCategory(_prefs.getCurrentSortCategory(), false);
_entryListView.setCopyBehavior(_prefs.getCopyBehavior());
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
_prefGroupFilter = _prefs.getGroupFilter();
View scrimOverlayLayout = LayoutInflater.from(this).inflate(R.layout.scrim_layout, null);
View scrimOverlay = scrimOverlayLayout.findViewById(R.id.scrim);
addContentView(scrimOverlayLayout, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
View fabMenuLayout = LayoutInflater.from(this).inflate(R.layout.fab_menu, null);
addContentView(fabMenuLayout, new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
ViewGroup menuItemsContainer = fabMenuLayout.findViewById(R.id.fab_menu_items_container);
FloatingActionButton fab = fabMenuLayout.findViewById(R.id.fab);
LinkedHashMap actions = new LinkedHashMap<>();
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan), this::startScanActivity);
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan_image), this::startScanImageActivity);
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_enter), this::startEditEntryActivity);
_fabMenuHelper = new FabMenuHelper(scrimOverlay, menuItemsContainer, fab, actions);
_fabMenuHelper.setOnFabMenuStateChangeListener(_fabMenuBackPressHandler::setEnabled);
_groupChip = findViewById(R.id.groupChipGroup);
_fabScrollHelper = new FabScrollHelper(fab);
_selectedEntries = new ArrayList<>();
}
public void setGroups(Collection groups) {
_groups = groups;
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
if (_prefGroupFilter != null) {
Set groupFilter = cleanGroupFilter(_prefGroupFilter);
_prefGroupFilter = null;
if (!groupFilter.isEmpty()) {
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
}
} else if (_groupFilter != null) {
Set groupFilter = cleanGroupFilter(_groupFilter);
if (!_groupFilter.equals(groupFilter)) {
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
}
}
_entryListView.setGroups(groups);
initializeGroups();
}
private void initializeGroups() {
_groupChip.removeAllViews();
_groupChip.setSingleSelection(!_prefs.isGroupMultiselectEnabled());
for (VaultGroup group : _groups) {
addChipTo(_groupChip, new VaultGroupModel(group));
}
GroupPlaceholderType placeholderType = GroupPlaceholderType.NO_GROUP;
addChipTo(_groupChip, new VaultGroupModel(this, placeholderType));
addSaveChip(_groupChip);
}
private Set cleanGroupFilter(Set groupFilter) {
Set groupUuids = _groups.stream().map(UUIDMap.Value::getUUID).collect(Collectors.toSet());
return groupFilter.stream()
.filter(g -> g == null || groupUuids.contains(g))
.collect(Collectors.toSet());
}
private void addChipTo(ChipGroup chipGroup, VaultGroupModel group) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(group.getName());
chip.setCheckable(true);
chip.setCheckedIconVisible(false);
chip.setChecked(_groupFilter != null && _groupFilter.contains(group.getUUID()));
if (group.isPlaceholder()) {
GroupPlaceholderType groupPlaceholderType = group.getPlaceholderType();
chip.setTag(groupPlaceholderType);
if (groupPlaceholderType == GroupPlaceholderType.ALL) {
chip.setChecked(_groupFilter == null);
} else if (groupPlaceholderType == GroupPlaceholderType.NO_GROUP) {
chip.setChecked(_groupFilter != null && _groupFilter.contains(null));
}
} else {
chip.setTag(group);
}
chip.setOnCheckedChangeListener((group1, isChecked) -> {
if (_actionMode != null) {
_actionMode.finish();
}
setSaveChipVisibility(true);
// Reset group filter if last checked group gets unchecked
if (!isChecked && _groupFilter.size() == 1) {
Set groupFilter = new HashSet<>();
chipGroup.clearCheck();
_groupFilter = groupFilter;
_entryListView.setGroupFilter(groupFilter);
return;
}
_groupFilter = getGroupFilter(chipGroup);
_entryListView.setGroupFilter(_groupFilter);
});
chipGroup.addView(chip);
}
private void addSaveChip(ChipGroup chipGroup) {
Chip chip = (Chip) getLayoutInflater().inflate(R.layout.chip_group_filter, null, false);
chip.setText(getString(R.string.save));
chip.setVisibility(View.GONE);
chip.setChipStrokeWidth(0);
chip.setCheckable(false);
chip.setChipBackgroundColorResource(android.R.color.transparent);
chip.setTextColor(MaterialColors.getColor(chip.getRootView(), com.google.android.material.R.attr.colorSecondary));
chip.setClickable(true);
chip.setCheckedIconVisible(false);
chip.setOnClickListener(v -> {
onSaveGroupFilter(_groupFilter);
setSaveChipVisibility(false);
});
chipGroup.addView(chip);
}
private void setSaveChipVisibility(boolean visible) {
Chip saveChip = (Chip) _groupChip.getChildAt(_groupChip.getChildCount() - 1);
saveChip.setChecked(false);
saveChip.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private static Set getGroupFilter(ChipGroup chipGroup) {
return chipGroup.getCheckedChipIds().stream()
.filter(Objects::nonNull)
.map(i -> {
Chip chip = chipGroup.findViewById(i);
if (chip.getTag() instanceof VaultGroupModel) {
VaultGroupModel group = (VaultGroupModel) chip.getTag();
return group.getUUID();
}
return null;
})
.collect(Collectors.toSet());
}
@Override
protected void onDestroy() {
_entryListView.setListener(null);
super.onDestroy();
}
@Override
protected void onPause() {
Map usageMap = _entryListView.getUsageCounts();
if (usageMap != null) {
_prefs.setUsageCount(usageMap);
}
Map lastUsedMap = _entryListView.getLastUsedTimestamps();
if (lastUsedMap != null) {
_prefs.setLastUsedTimestamps(lastUsedMap);
}
super.onPause();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle instance) {
super.onSaveInstanceState(instance);
instance.putString("pendingSearchQuery", _pendingSearchQuery);
instance.putString("submittedSearchQuery", _submittedSearchQuery);
instance.putBoolean("isDoingIntro", _isDoingIntro);
instance.putBoolean("isAuthenticating", _isAuthenticating);
if (_groupFilter != null) {
instance.putSerializable("prefGroupFilter", new HashSet<>(_groupFilter));
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!PermissionHelper.checkResults(grantResults)) {
Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_SHORT).show();
return;
}
if (requestCode == CODE_PERM_CAMERA) {
startScanActivity();
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
_isDPadPressed = isDPadKey(keyCode);
return super.onKeyDown(keyCode, event);
}
private static boolean isDPadKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT;
}
@Override
public void onEntryListTouch() {
_isDPadPressed = false;
if (_searchView != null && !_searchView.isIconified()) {
if (ViewCompat.getRootWindowInsets(findViewById(android.R.id.content).getRootView()).isVisible(WindowInsetsCompat.Type.ime())) {
InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null && getCurrentFocus() != null) {
inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
}
}
}
private void onPreferencesResult() {
// refresh the entire entry list if needed
if (_loaded) {
recreate();
}
}
private void startEditEntryActivity() {
String clip = ClipboardUtils.readText(this);
if (clip != null) {
GoogleAuthInfo parsed;
try {
parsed = GoogleAuthInfo.parseUri(clip.trim());
String message = getString(
R.string.import_from_clipboard_message,
parsed.getAccountName(),
parsed.getIssuer()
);
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this)
.setTitle(R.string.import_from_clipboard_title)
.setMessage(message)
.setPositiveButton(R.string.yes, (dialog, which) -> startEditEntryActivityForNew(new VaultEntry(parsed)))
.setNegativeButton(R.string.no, (dialog, which) -> startEditEntryActivityForManual())
.create());
return;
} catch (GoogleAuthInfoException e) {
Log.i("EntryActivity", "Clipboard did not contain a valid otpauth URI", e);
}
}
startEditEntryActivityForManual();
}
private void startEditEntryActivityForNew(VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", entry);
intent.putExtra("isManual", false);
addEntryResultLauncher.launch(intent);
}
private void startEditEntryActivityForManual() {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", VaultEntry.getDefault());
intent.putExtra("isManual", true);
addEntryResultLauncher.launch(intent);
}
private void startEditEntryActivity(VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entryUUID", entry.getUUID());
editEntryResultLauncher.launch(intent);
}
private void startAssignIconsActivity(List entries) {
ArrayList assignIconEntriesIds = new ArrayList<>();
Intent assignIconIntent = new Intent(getBaseContext(), AssignIconsActivity.class);
for (VaultEntry entry : entries) {
assignIconEntriesIds.add(entry.getUUID());
}
assignIconIntent.putExtra("entries", assignIconEntriesIds);
assignIconsResultLauncher.launch(assignIconIntent);
}
private void startAssignGroupsDialog() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_select_group, null);
TextInputLayout groupSelectionLayout = view.findViewById(R.id.group_selection_layout);
AutoCompleteTextView groupsSelection = view.findViewById(R.id.group_selection_dropdown);
TextInputLayout newGroupLayout = view.findViewById(R.id.text_group_name_layout);
TextInputEditText newGroupText = view.findViewById(R.id.text_group_name);
Collection groups = _vaultManager.getVault().getUsedGroups();
List groupModels = new ArrayList<>();
groupModels.add(new VaultGroupModel(this, GroupPlaceholderType.NEW_GROUP));
groupModels.addAll(groups.stream().map(VaultGroupModel::new).collect(Collectors.toList()));
DropdownHelper.fillDropdown(this, groupsSelection, groupModels);
AtomicReference groupModelRef = new AtomicReference<>();
groupsSelection.setOnItemClickListener((parent, view1, position, id) -> {
VaultGroupModel groupModel = (VaultGroupModel) parent.getItemAtPosition(position);
groupModelRef.set(groupModel);
if (groupModel.isPlaceholder()) {
newGroupLayout.setVisibility(View.VISIBLE);
newGroupText.requestFocus();
} else {
newGroupLayout.setVisibility(View.GONE);
}
groupSelectionLayout.setError(null);
});
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.assign_groups)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
btnPos.setOnClickListener(v -> {
VaultGroupModel groupModel = groupModelRef.get();
if (groupModel == null) {
groupSelectionLayout.setError(getString(R.string.error_required_field));
return;
}
if (groupModel.isPlaceholder()) {
String newGroupName = newGroupText.getText().toString().trim();
if (newGroupName.isEmpty()) {
newGroupLayout.setError(getString(R.string.error_required_field));
return;
}
VaultGroup group = new VaultGroup(newGroupName);
_vaultManager.getVault().addGroup(group);
groupModel = new VaultGroupModel(group);
}
for (VaultEntry selectedEntry : _selectedEntries) {
selectedEntry.addGroup(groupModel.getUUID());
}
dialog.dismiss();
saveAndBackupVault();
_actionMode.finish();
setGroups(_vaultManager.getVault().getUsedGroups());
});
});
Dialogs.showSecureDialog(dialog);
}
private void startIntroActivity() {
if (!_isDoingIntro) {
Intent intro = new Intent(this, IntroActivity.class);
introResultLauncher.launch(intro);
_isDoingIntro = true;
}
}
private void onScanResult(Intent data) {
List entries = (ArrayList) data.getSerializableExtra("entries");
if (entries != null) {
importScannedEntries(entries);
}
}
private void onAddEntryResult(Intent data) {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(entryUUID);
_entryListView.setEntries(_vaultManager.getVault().getEntries());
_entryListView.onEntryAdded(entry);
}
}
private void onEditEntryResult() {
if (_loaded) {
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void onAssignIconsResult() {
if (_loaded) {
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void onScanImageResult(Intent intent) {
if (intent.getData() != null) {
startDecodeQrCodeImages(Collections.singletonList(intent.getData()));
return;
}
if (intent.getClipData() != null) {
ClipData data = intent.getClipData();
List uris = new ArrayList<>();
for (int i = 0; i < data.getItemCount(); i++) {
ClipData.Item item = data.getItemAt(i);
if (item.getUri() != null) {
uris.add(item.getUri());
}
}
if (uris.size() > 0) {
startDecodeQrCodeImages(uris);
}
}
}
private static CharSequence buildImportError(String fileName, Throwable e) {
SpannableStringBuilder builder = new SpannableStringBuilder(String.format("%s:\n%s", fileName, e));
builder.setSpan(new StyleSpan(Typeface.BOLD), 0, fileName.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return builder;
}
private void startDecodeQrCodeImages(List uris) {
QrDecodeTask task = new QrDecodeTask(this, (results) -> {
List errors = new ArrayList<>();
List entries = new ArrayList<>();
List googleAuthExports = new ArrayList<>();
for (QrDecodeTask.Result res : results) {
if (res.getException() != null) {
errors.add(buildImportError(res.getFileName(), res.getException()));
continue;
}
try {
Uri scanned = Uri.parse(res.getResult().getText());
if (Objects.equals(scanned.getScheme(), GoogleAuthInfo.SCHEME_EXPORT)) {
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(scanned);
for (GoogleAuthInfo info: export.getEntries()) {
VaultEntry entry = new VaultEntry(info);
entries.add(entry);
}
googleAuthExports.add(export);
} else {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(res.getResult().getText());
VaultEntry entry = new VaultEntry(info);
entries.add(entry);
}
} catch (GoogleAuthInfoException e) {
errors.add(buildImportError(res.getFileName(), e));
}
}
final DialogInterface.OnClickListener dialogDismissHandler = (dialog, which) -> importScannedEntries(entries);
if (!googleAuthExports.isEmpty()) {
boolean isSingleBatch = GoogleAuthInfo.Export.isSingleBatch(googleAuthExports);
if (!isSingleBatch && errors.size() > 0) {
errors.add(getString(R.string.unrelated_google_auth_batches_error));
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.no_tokens_can_be_imported), errors, null);
return;
} else if (!isSingleBatch) {
Dialogs.showErrorDialog(this, R.string.import_google_auth_failure, getString(R.string.unrelated_google_auth_batches_error));
return;
} else {
List missingIndices = GoogleAuthInfo.Export.getMissingIndices(googleAuthExports);
if (missingIndices.size() != 0) {
Dialogs.showPartialGoogleAuthImportWarningDialog(this, missingIndices, entries.size(), errors, dialogDismissHandler);
return;
}
}
}
if ((errors.size() > 0 && results.size() > 1) || errors.size() > 1) {
Dialogs.showMultiErrorDialog(this, R.string.import_error_title, getString(R.string.unable_to_read_qrcode_files, uris.size() - errors.size(), uris.size()), errors, dialogDismissHandler);
} else if (errors.size() > 0) {
Dialogs.showErrorDialog(this, getString(R.string.unable_to_read_qrcode_file, results.get(0).getFileName()), errors.get(0), dialogDismissHandler);
} else {
importScannedEntries(entries);
}
});
task.execute(getLifecycle(), uris);
}
private void importScannedEntries(List entries) {
if (entries.size() == 1) {
startEditEntryActivityForNew(entries.get(0));
} else if (entries.size() > 1) {
for (VaultEntry entry: entries) {
_vaultManager.getVault().addEntry(entry);
}
if (saveAndBackupVault()) {
Toast.makeText(this, getResources().getQuantityString(R.plurals.added_new_entries, entries.size(), entries.size()), Toast.LENGTH_LONG).show();
}
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void updateSortCategoryMenu() {
if (_menu != null) {
SortCategory category = _prefs.getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
}
private void onIntroResult() {
loadEntries();
}
private void checkTimeSyncSetting() {
boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1;
if (!autoTime && _prefs.isTimeSyncWarningEnabled()) {
Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
startActivity(intent);
});
}
}
private void checkIconOptimization() {
if (!_vaultManager.getVault().areIconsOptimized()) {
Map oldIcons = _vaultManager.getVault().getEntries().stream()
.filter(e -> e.getIcon() != null
&& !e.getIcon().getType().equals(IconType.SVG)
&& !BitmapHelper.isVaultEntryIconOptimized(e.getIcon()))
.collect(Collectors.toMap(VaultEntry::getUUID, VaultEntry::getIcon));
if (!oldIcons.isEmpty()) {
IconOptimizationTask task = new IconOptimizationTask(this, this::onIconsOptimized);
task.execute(getLifecycle(), oldIcons);
} else {
onIconsOptimized(Collections.emptyMap());
}
}
}
private void onIconsOptimized(Map newIcons) {
for (Map.Entry mapEntry : newIcons.entrySet()) {
VaultEntry entry = _vaultManager.getVault().getEntryByUUID(mapEntry.getKey());
entry.setIcon(mapEntry.getValue());
}
_vaultManager.getVault().setIconsOptimized(true);
saveAndBackupVault();
if (!newIcons.isEmpty()) {
_entryListView.setEntries(_vaultManager.getVault().getEntries());
}
}
private void onDecryptResult() {
_auditLogRepository.addVaultUnlockedEvent();
loadEntries();
}
private void startScanActivity() {
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
return;
}
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
scanResultLauncher.launch(scannerActivity);
}
private void startScanImageActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
fileIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
fileIntent.setType("image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
_vaultManager.fireIntentLauncher(this, chooserIntent, codeScanResultLauncher);
}
private void startPreferencesActivity() {
startPreferencesActivity(null, null);
}
private void startPreferencesActivity(Class extends PreferencesFragment> fragmentType, String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("fragment", fragmentType);
intent.putExtra("pref", preference);
preferenceResultLauncher.launch(intent);
}
private void doShortcutActions() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null || !_vaultManager.isVaultLoaded()) {
return;
}
switch (action) {
case "scan":
startScanActivity();
break;
}
intent.removeExtra("action");
}
private void handleIncomingIntent() {
if (!_vaultManager.isVaultLoaded()) {
return;
}
Intent intent = getIntent();
if (intent.getAction() == null) {
return;
}
Uri uri;
switch (intent.getAction()) {
case Intent.ACTION_VIEW:
uri = intent.getData();
if (uri != null) {
intent.setData(null);
intent.setAction(null);
GoogleAuthInfo info;
try {
info = GoogleAuthInfo.parseUri(uri);
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_process_deeplink, e);
break;
}
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(entry);
}
break;
case Intent.ACTION_SEND:
if (intent.hasExtra(Intent.EXTRA_STREAM)) {
uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_STREAM);
if (uri != null) {
startDecodeQrCodeImages(Collections.singletonList(uri));
}
}
if (intent.hasExtra(Intent.EXTRA_TEXT)) {
String stringExtra = intent.getStringExtra(Intent.EXTRA_TEXT);
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_TEXT);
if (stringExtra != null) {
GoogleAuthInfo info;
try {
info = GoogleAuthInfo.parseUri(stringExtra);
} catch (GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_process_shared_text, e);
break;
}
VaultEntry entry = new VaultEntry(info);
startEditEntryActivityForNew(entry);
}
}
break;
case Intent.ACTION_SEND_MULTIPLE:
if (intent.hasExtra(Intent.EXTRA_STREAM)) {
List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_STREAM);
if (uris != null) {
uris = uris.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
startDecodeQrCodeImages(uris);
}
}
break;
}
}
@Override
protected void onStop() {
super.onStop();
_entryListView.onRefreshStop();
}
@Override
protected void onStart() {
super.onStart();
if (_vaultManager.isVaultInitNeeded()) {
if (_prefs.isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
startIntroActivity();
return;
}
// If the vault is not loaded yet, try to load it now in case it's plain text
if (!_vaultManager.isVaultLoaded()) {
VaultFile vaultFile;
try {
vaultFile = VaultRepository.readVaultFile(this);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
if (!vaultFile.isEncrypted()) {
try {
_vaultManager.loadFrom(vaultFile);
} catch (VaultRepositoryException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> {
finish();
});
return;
}
}
}
if (!_vaultManager.isVaultLoaded()) {
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the entry list view so that the chip gets updated
setGroups(_vaultManager.getVault().getUsedGroups());
// update the usage counts in case they are edited outside of the EntryListView
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
_entryListView.onRefreshStart();
} else {
loadEntries();
checkTimeSyncSetting();
checkIconOptimization();
_entryListView.onRefreshStart();
}
_lockBackPressHandler.setEnabled(
_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)
);
handleIncomingIntent();
updateLockIcon();
updateSortCategoryMenu();
doShortcutActions();
updateErrorCard();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
updateSortCategoryMenu();
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
_searchView = (SearchView) searchViewMenuItem.getActionView();
_searchView.setMaxWidth(Integer.MAX_VALUE);
_searchView.setOnQueryTextFocusChangeListener((v, hasFocus) -> {
boolean enabled = _submittedSearchQuery != null || hasFocus;
_searchViewBackPressHandler.setEnabled(enabled);
});
_searchView.setOnCloseListener(() -> {
boolean enabled = _submittedSearchQuery != null;
_searchViewBackPressHandler.setEnabled(enabled);
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
return false;
});
_searchView.setQueryHint(getString(R.string.search));
_searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
setTitle(getString(R.string.search));
getSupportActionBar().setSubtitle(s);
_entryListView.setSearchFilter(s);
_pendingSearchQuery = null;
_submittedSearchQuery = s;
collapseSearchView();
_searchViewBackPressHandler.setEnabled(true);
return false;
}
@Override
public boolean onQueryTextChange(String s) {
if (_submittedSearchQuery == null) {
_entryListView.setSearchFilter(s);
}
_pendingSearchQuery = Strings.isNullOrEmpty(s) && !_searchView.isIconified() ? null : s;
if (_pendingSearchQuery != null) {
_entryListView.setSearchFilter(_pendingSearchQuery);
}
return false;
}
});
_searchView.setOnSearchClickListener(v -> {
String query = _submittedSearchQuery != null ? _submittedSearchQuery : _pendingSearchQuery;
_groupChip.setVisibility(View.GONE);
_searchView.setQuery(query, false);
});
if (_pendingSearchQuery != null) {
_searchView.setIconified(false);
_searchView.setQuery(_pendingSearchQuery, false);
_searchViewBackPressHandler.setEnabled(true);
} else if (_submittedSearchQuery != null) {
setTitle(getString(R.string.search));
getSupportActionBar().setSubtitle(_submittedSearchQuery);
_entryListView.setSearchFilter(_submittedSearchQuery);
_searchViewBackPressHandler.setEnabled(true);
} else if (_prefs.getFocusSearchEnabled() && !_isRecreated) {
_searchView.setIconified(false);
_searchView.setFocusable(true);
_searchView.requestFocus();
_searchView.requestFocusFromTouch();
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
startPreferencesActivity();
} else if (itemId == R.id.action_about) {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
} else if (itemId == R.id.action_lock) {
_vaultManager.lock(true);
} else {
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
SortCategory sortCategory;
int subItemId = item.getItemId();
if (subItemId == R.id.menu_sort_alphabetically) {
sortCategory = SortCategory.ISSUER;
} else if (subItemId == R.id.menu_sort_alphabetically_reverse) {
sortCategory = SortCategory.ISSUER_REVERSED;
} else if (subItemId == R.id.menu_sort_alphabetically_name) {
sortCategory = SortCategory.ACCOUNT;
} else if (subItemId == R.id.menu_sort_alphabetically_name_reverse) {
sortCategory = SortCategory.ACCOUNT_REVERSED;
} else if (subItemId == R.id.menu_sort_usage_count) {
sortCategory = SortCategory.USAGE_COUNT;
} else if (subItemId == R.id.menu_sort_last_used) {
sortCategory = SortCategory.LAST_USED;
} else {
sortCategory = SortCategory.CUSTOM;
}
_entryListView.setSortCategory(sortCategory, true);
_prefs.setCurrentSortCategory(sortCategory);
}
return super.onOptionsItemSelected(item);
}
return true;
}
private void collapseSearchView() {
_groupChip.setVisibility(_groups.isEmpty() ? View.GONE : View.VISIBLE);
_searchView.setQuery(null, false);
_searchView.setIconified(true);
}
private void loadEntries() {
if (!_loaded) {
setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setUsageCounts(_prefs.getUsageCounts());
_entryListView.setLastUsedTimestamps(_prefs.getLastUsedTimestamps());
_entryListView.setEntries(_vaultManager.getVault().getEntries());
if (!_isRecreated) {
_entryListView.runEntriesAnimation();
}
_loaded = true;
}
}
private void startAuthActivity(boolean inhibitBioPrompt) {
if (!_isAuthenticating) {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
authResultLauncher.launch(intent);
_isAuthenticating = true;
}
}
private void updateLockIcon() {
// hide the lock icon if the vault is not unlocked
if (_menu != null && _vaultManager.isVaultLoaded()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_vaultManager.getVault().isEncryptionEnabled());
}
}
private void updateErrorCard() {
ErrorCardInfo info = null;
Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
if (backupRes != null) {
info = new ErrorCardInfo(getString(R.string.backup_error_bar_message), view -> {
Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
});
} else if (_prefs.isBackupsReminderNeeded() && _prefs.isBackupReminderEnabled()) {
String text;
Date date = _prefs.getLatestBackupOrExportTime();
if (date != null) {
text = getString(R.string.backup_reminder_bar_message_with_latest, TimeUtils.getElapsedSince(this, date));
} else {
text = getString(R.string.backup_reminder_bar_message);
}
info = new ErrorCardInfo(text, view -> {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Error)
.setTitle(R.string.backup_reminder_bar_dialog_title)
.setMessage(R.string.backup_reminder_bar_dialog_summary)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(R.string.backup_reminder_bar_dialog_accept, (dialog, whichButton) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
})
.setNegativeButton(android.R.string.cancel, null)
.create());
});
} else if (_prefs.isPlaintextBackupWarningNeeded()) {
info = new ErrorCardInfo(getString(R.string.backup_plaintext_export_warning), view -> showPlaintextExportWarningOptions());
}
_entryListView.setErrorCardInfo(info);
}
private void showPlaintextExportWarningOptions() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_plaintext_warning, null);
AlertDialog dialog = new MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.backup_plaintext_export_warning)
.setView(view)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
CheckBox checkBox = view.findViewById(R.id.checkbox_plaintext_warning);
checkBox.setChecked(false);
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
btnPos.setOnClickListener(l -> {
dialog.dismiss();
_prefs.setIsPlaintextBackupWarningDisabled(checkBox.isChecked());
_prefs.setIsPlaintextBackupWarningNeeded(false);
updateErrorCard();
});
});
Dialogs.showSecureDialog(dialog);
}
@Override
public void onRestoreInstanceState(@Nullable Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState == null) {
return;
}
HashSet filter = (HashSet) savedInstanceState.getSerializable("prefGroupFilter");
if (filter != null) {
_prefGroupFilter = filter;
}
}
@Override
public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) {
if (_selectedEntries.isEmpty()) {
_actionMode.finish();
} else {
setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1);
}
}
}
@Override
public void onSelect(VaultEntry entry) {
_selectedEntries.add(entry);
}
@Override
public void onDeselect(VaultEntry entry) {
_selectedEntries.remove(entry);
}
private void setIsMultipleSelected(boolean multipleSelected) {
_entryListView.setIsLongPressDragEnabled(!multipleSelected);
_actionMode.getMenu().findItem(R.id.action_edit).setVisible(!multipleSelected);
_actionMode.getMenu().findItem(R.id.action_copy).setVisible(!multipleSelected);
}
private void setAssignIconsMenuItemVisibility() {
MenuItem assignIconsMenuItem = _actionMode.getMenu().findItem(R.id.action_assign_icons);
assignIconsMenuItem.setVisible(_iconPackManager.hasIconPack());
}
private void setFavoriteMenuItemVisiblity() {
MenuItem toggleFavoriteMenuItem = _actionMode.getMenu().findItem(R.id.action_toggle_favorite);
if (_selectedEntries.size() == 1){
if (_selectedEntries.get(0).isFavorite()) {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_filled_star_24);
toggleFavoriteMenuItem.setTitle(R.string.unfavorite);
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
toggleFavoriteMenuItem.setTitle(R.string.favorite);
}
} else {
toggleFavoriteMenuItem.setIcon(R.drawable.ic_outline_star_24);
toggleFavoriteMenuItem.setTitle(String.format("%s / %s", getString(R.string.favorite), getString(R.string.unfavorite)));
}
}
@Override
public void onLongEntryClick(VaultEntry entry) {
if (!_selectedEntries.isEmpty()) {
return;
}
_selectedEntries.add(entry);
_entryListView.setActionModeState(true, entry);
startActionMode();
}
private void startActionMode() {
_actionMode = startSupportActionMode(_actionModeCallbacks);
_actionModeBackPressHandler.setEnabled(true);
setFavoriteMenuItemVisiblity();
setAssignIconsMenuItemVisibility();
}
@Override
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vaultManager.getVault().moveEntry(entry1, entry2);
}
@Override
public void onEntryDrop(VaultEntry entry) {
saveVault();
}
@Override
public void onEntryChange(VaultEntry entry) {
saveAndBackupVault();
}
public void onEntryCopy(VaultEntry entry) {
copyEntryCode(entry);
}
@Override
public void onScroll(int dx, int dy) {
if (!_isDPadPressed) {
_fabScrollHelper.onScroll(dx, dy);
}
}
@Override
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onSaveGroupFilter(Set groupFilter) {
if (_vaultManager.getVault().isGroupsMigrationFresh()) {
saveAndBackupVault();
}
_prefs.setGroupFilter(groupFilter);
}
@Override
public void onLocked(boolean userInitiated) {
if (_actionMode != null) {
_actionMode.finish();
}
if (_searchView != null && !_searchView.isIconified()) {
collapseSearchView();
}
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
_fabMenuHelper.close();
}
_entryListView.clearEntries();
_loaded = false;
if (userInitiated) {
startAuthActivity(true);
} else {
super.onLocked(false);
}
}
@Override
protected boolean saveAndBackupVault() {
boolean res = super.saveAndBackupVault();
updateErrorCard();
return res;
}
@SuppressLint("InlinedApi")
private void copyEntryCode(VaultEntry entry) {
String otp;
try {
otp = entry.getInfo().getOtp();
} catch (OtpInfoException e) {
return;
}
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", otp);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PersistableBundle extras = new PersistableBundle();
extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
clip.getDescription().setExtras(extras);
}
clipboard.setPrimaryClip(clip);
if (_prefs.isMinimizeOnCopyEnabled()) {
moveTaskToBack(true);
}
}
private class SearchViewBackPressHandler extends OnBackPressedCallback {
public SearchViewBackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
if (!_searchView.isIconified() || _submittedSearchQuery != null) {
_submittedSearchQuery = null;
_pendingSearchQuery = null;
_entryListView.setSearchFilter(null);
collapseSearchView();
setTitle(R.string.app_name);
getSupportActionBar().setSubtitle(null);
}
}
}
private class LockBackPressHandler extends OnBackPressedCallback {
public LockBackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
if (_vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
_vaultManager.lock(false);
}
}
}
private class ActionModeBackPressHandler extends OnBackPressedCallback {
public ActionModeBackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
if (_actionMode != null) {
_actionMode.finish();
}
}
}
private class FabMenuBackPressHandler extends OnBackPressedCallback {
public FabMenuBackPressHandler() {
super(false);
}
@Override
public void handleOnBackPressed() {
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
_fabMenuHelper.close();
}
}
}
private class ActionModeCallbacks implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (_selectedEntries.size() == 0) {
mode.finish();
return true;
}
int itemId = item.getItemId();
if (itemId == R.id.action_copy) {
copyEntryCode(_selectedEntries.get(0));
mode.finish();
} else if (itemId == R.id.action_edit) {
startEditEntryActivity(_selectedEntries.get(0));
mode.finish();
} else if (itemId == R.id.action_toggle_favorite) {
for (VaultEntry entry : _selectedEntries) {
_vaultManager.getVault().editEntry(entry, newEntry -> {
newEntry.setIsFavorite(!newEntry.isFavorite());
});
}
saveAndBackupVault();
_entryListView.setEntries(_vaultManager.getVault().getEntries());
mode.finish();
} else if (itemId == R.id.action_share_qr) {
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList authInfos = new ArrayList<>();
for (VaultEntry entry : _selectedEntries) {
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
authInfos.add(authInfo);
_auditLogRepository.addEntrySharedEvent(entry.getUUID().toString());
}
intent.putExtra("authInfos", authInfos);
startActivity(intent);
mode.finish();
} else if (itemId == R.id.action_delete) {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries, (d, which) -> {
for (VaultEntry entry : _selectedEntries) {
_vaultManager.getVault().removeEntry(entry);
}
saveAndBackupVault();
_entryListView.setGroups(_vaultManager.getVault().getUsedGroups());
_entryListView.setEntries(_vaultManager.getVault().getEntries());
mode.finish();
});
} else if (itemId == R.id.action_select_all) {
_selectedEntries = _entryListView.selectAllEntries();
setFavoriteMenuItemVisiblity();
setIsMultipleSelected(_selectedEntries.size() > 1);
} else if (itemId == R.id.action_assign_icons) {
startAssignIconsActivity(_selectedEntries);
mode.finish();
} else if (itemId == R.id.action_assign_groups) {
startAssignGroupsDialog();
} else {
return false;
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_actionModeBackPressHandler.setEnabled(false);
_selectedEntries.clear();
_actionMode = null;
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/PanicResponderActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.pins.GuardianProjectFDroidRSA2048;
import com.beemdevelopment.aegis.vault.VaultRepository;
import info.guardianproject.GuardianProjectRSA4096;
import info.guardianproject.trustedintents.TrustedIntents;
public class PanicResponderActivity extends AegisActivity {
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!_prefs.isPanicTriggerEnabled()) {
Toast.makeText(this, R.string.panic_trigger_ignore_toast, Toast.LENGTH_SHORT).show();
finish();
return;
}
Intent intent;
if (!BuildConfig.TEST.get()) {
TrustedIntents trustedIntents = TrustedIntents.get(this);
trustedIntents.addTrustedSigner(GuardianProjectRSA4096.class);
trustedIntents.addTrustedSigner(GuardianProjectFDroidRSA2048.class);
intent = trustedIntents.getIntentFromTrustedSender(this);
} else {
intent = getIntent();
}
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
VaultRepository.deleteFile(this);
_vaultManager.lock(false);
finishApp();
return;
}
finish();
}
private void finishApp() {
ExitActivity.exitAppAndRemoveFromRecents(this);
finishAndRemoveTask();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.fragments.preferences.AppearancePreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.preferences.PreferencesFragment;
import com.beemdevelopment.aegis.helpers.ViewHelper;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private Fragment _fragment;
private CharSequence _prefTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_preferences);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(new FragmentResumeListener(), true);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (savedInstanceState == null) {
_fragment = new MainPreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, _fragment)
.commit();
PreferencesFragment requestedFragment = getRequestedFragment();
if (requestedFragment != null) {
_fragment = requestedFragment;
showFragment(_fragment);
}
} else {
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
_prefTitle = savedInstanceState.getCharSequence("prefTitle");
if (_prefTitle != null) {
setTitle(_prefTitle);
}
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
outState.putCharSequence("prefTitle", _prefTitle);
super.onSaveInstanceState(outState);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public boolean onPreferenceStartFragment(@NonNull PreferenceFragmentCompat caller, Preference pref) {
_fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
_fragment.setArguments(pref.getExtras());
_fragment.setTargetFragment(caller, 0);
showFragment(_fragment);
_prefTitle = pref.getTitle();
setTitle(_prefTitle);
return true;
}
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.content, fragment)
.addToBackStack(null)
.commit();
}
@SuppressWarnings("unchecked")
private PreferencesFragment getRequestedFragment() {
Class extends PreferencesFragment> fragmentType = (Class extends PreferencesFragment>) getIntent().getSerializableExtra("fragment");
if (fragmentType == null) {
return null;
}
try {
return fragmentType.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
private class FragmentResumeListener extends FragmentManager.FragmentLifecycleCallbacks {
@Override
public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {
if (f instanceof MainPreferencesFragment) {
setTitle(R.string.action_settings);
} else if (f instanceof AppearancePreferencesFragment) {
_prefTitle = getString(R.string.pref_section_appearance_title);
setTitle(_prefTitle);
}
}
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/ScannerActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
private ProcessCameraProvider _cameraProvider;
private ListenableFuture _cameraProviderFuture;
private List _lenses;
private int _currentLens;
private Menu _menu;
private ImageAnalysis _analysis;
private PreviewView _previewView;
private ExecutorService _executor;
private int _batchId = 0;
private int _batchIndex = -1;
private List _entries;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_scanner);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_entries = new ArrayList<>();
_lenses = new ArrayList<>();
_previewView = findViewById(R.id.preview_view);
_executor = Executors.newSingleThreadExecutor();
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
_cameraProviderFuture.addListener(() -> {
try {
_cameraProvider = _cameraProviderFuture.get();
} catch (ExecutionException | InterruptedException e) {
// if we're to believe the Android documentation, this should never happen
// https://developer.android.com/training/camerax/preview#check-provider
throw new RuntimeException(e);
}
addCamera(CameraSelector.LENS_FACING_BACK);
addCamera(CameraSelector.LENS_FACING_FRONT);
if (_lenses.size() == 0) {
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
finish();
return;
}
_currentLens = _lenses.get(0);
updateCameraIcon();
bindPreview(_cameraProvider);
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void onDestroy() {
if (_executor != null) {
_executor.shutdownNow();
}
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_scanner, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (_cameraProvider == null) {
return false;
}
if (item.getItemId() == R.id.action_camera) {
unbindPreview(_cameraProvider);
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
bindPreview(_cameraProvider);
updateCameraIcon();
return true;
}
return super.onOptionsItemSelected(item);
}
private void addCamera(int lens) {
try {
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
if (_cameraProvider.hasCamera(camera)) {
_lenses.add(lens);
}
} catch (CameraInfoUnavailableException e) {
e.printStackTrace();
}
}
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = _lenses.size() > 1;
if (dual) {
switch (_currentLens) {
case CameraSelector.LENS_FACING_BACK:
item.setIcon(R.drawable.ic_outline_camera_front_24);
break;
case CameraSelector.LENS_FACING_FRONT:
item.setIcon(R.drawable.ic_outline_camera_rear_24);
break;
}
}
item.setVisible(dual);
}
}
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(_previewView.getSurfaceProvider());
CameraSelector selector = new CameraSelector.Builder()
.requireLensFacing(_currentLens)
.build();
_analysis = new ImageAnalysis.Builder()
.setTargetResolution(QrCodeAnalyzer.RESOLUTION)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
_analysis.setAnalyzer(_executor, new QrCodeAnalyzer(this));
cameraProvider.bindToLifecycle(this, selector, preview, _analysis);
}
private void unbindPreview(@NonNull ProcessCameraProvider cameraProvider) {
_analysis = null;
cameraProvider.unbindAll();
}
@Override
public void onQrCodeDetected(Result result) {
new Handler(getMainLooper()).post(() -> {
if (isFinishing()) {
return;
}
if (_analysis != null) {
try {
Uri uri = Uri.parse(result.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
}
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
unbindPreview(_cameraProvider);
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
}
}
});
}
private void handleUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
List entries = new ArrayList<>();
entries.add(new VaultEntry(info));
finish(entries);
}
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
if (_batchId == 0) {
_batchId = export.getBatchId();
}
int batchIndex = export.getBatchIndex();
if (_batchId != export.getBatchId()) {
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
for (GoogleAuthInfo info : export.getEntries()) {
VaultEntry entry = new VaultEntry(info);
_entries.add(entry);
}
_batchIndex = batchIndex;
if (_batchIndex + 1 == export.getBatchSize()) {
finish(_entries);
}
Toast.makeText(this, getResources().getQuantityString(R.plurals.google_qr_export_scanned, export.getBatchSize(), _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
} else if (_batchIndex != batchIndex) {
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
}
}
private void finish(List entries) {
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList) entries);
setResult(RESULT_OK, intent);
finish();
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/TransferEntriesActivity.java
================================================
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.provider.Settings;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.QrCodeHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.Transferable;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.helpers.ViewHelper;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.imageview.ShapeableImageView;
import com.google.zxing.WriterException;
import java.util.ArrayList;
import java.util.List;
public class TransferEntriesActivity extends AegisActivity {
private List _authInfos;
private ShapeableImageView _qrImage;
private TextView _description;
private TextView _issuer;
private TextView _accountName;
private TextView _entriesCount;
private Button _nextButton;
private Button _previousButton;
private Button _copyButton;
private int _currentEntryCount = 1;
private float _deviceBrightness;
private boolean _isMaxBrightnessSet = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (abortIfOrphan(savedInstanceState)) {
return;
}
setContentView(R.layout.activity_share_entry);
setSupportActionBar(findViewById(R.id.toolbar));
ViewHelper.setupAppBarInsets(findViewById(R.id.app_bar_layout));
_qrImage = findViewById(R.id.ivQrCode);
_description = findViewById(R.id.tvDescription);
_issuer = findViewById(R.id.tvIssuer);
_accountName = findViewById(R.id.tvAccountName);
_entriesCount = findViewById(R.id.tvEntriesCount);
_nextButton = findViewById(R.id.btnNext);
_previousButton = findViewById(R.id.btnPrevious);
_copyButton = findViewById(R.id.btnCopyClipboard);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Intent intent = getIntent();
_authInfos = (ArrayList) intent.getSerializableExtra("authInfos");
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
_nextButton.setVisibility(controlVisibility);
_nextButton.setOnClickListener(v -> {
if (_currentEntryCount < _authInfos.size()) {
_previousButton.setVisibility(View.VISIBLE);
_currentEntryCount++;
generateQR();
if (_currentEntryCount == _authInfos.size()) {
_nextButton.setText(R.string.done);
}
} else {
finish();
}
});
_previousButton.setOnClickListener(v -> {
if (_currentEntryCount > 1) {
_nextButton.setText(R.string.next);
_currentEntryCount--;
generateQR();
if (_currentEntryCount == 1) {
_previousButton.setVisibility(View.INVISIBLE);
}
}
});
if (_authInfos.get(0) instanceof GoogleAuthInfo) {
_copyButton.setVisibility(View.VISIBLE);
}
_copyButton.setOnClickListener(v -> {
Transferable selectedEntry = _authInfos.get(_currentEntryCount - 1);
try {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", selectedEntry.getUri().toString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
PersistableBundle extras = new PersistableBundle();
extras.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true);
clip.getDescription().setExtras(extras);
}
if (clipboard != null) {
clipboard.setPrimaryClip(clip);
}
Toast.makeText(this, R.string.uri_copied_to_clipboard, Toast.LENGTH_SHORT).show();
} catch (GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_copy_uri_to_clipboard, e);
}
});
// Calculate sensible dimensions for the QR code depending on whether we're in landscape
_qrImage.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ConstraintLayout layout = findViewById(R.id.layoutShareEntry);
if (layout.getWidth() > layout.getHeight()) {
int squareSize = (int) (0.5 * layout.getHeight());
ViewGroup.LayoutParams params = _qrImage.getLayoutParams();
params.width = squareSize;
params.height = squareSize;
_qrImage.setLayoutParams(params);
}
generateQR();
_qrImage.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
_deviceBrightness = getSystemBrightness();
_qrImage.setOnClickListener(v -> {
if (!_isMaxBrightnessSet) {
setBrightness(1f);
_isMaxBrightnessSet = true;
} else {
setBrightness(_deviceBrightness);
_isMaxBrightnessSet = false;
}
});
}
private float getSystemBrightness() {
int brightness = 0;
try {
brightness = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
return brightness / 255f;
}
private void setBrightness(float brightnessAmount) {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.screenBrightness = brightnessAmount;
getWindow().setAttributes(attrs);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
private void generateQR() {
Transferable selectedEntry = _authInfos.get(_currentEntryCount - 1);
if (selectedEntry instanceof GoogleAuthInfo) {
GoogleAuthInfo entry = (GoogleAuthInfo) selectedEntry;
_issuer.setText(entry.getIssuer());
_accountName.setText(entry.getAccountName());
} else if (selectedEntry instanceof GoogleAuthInfo.Export) {
_description.setText(R.string.google_auth_compatible_transfer_description);
}
_entriesCount.setText(getResources().getQuantityString(R.plurals.qr_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
int backgroundColor = _themeHelper.getConfiguredTheme() == Theme.LIGHT
? MaterialColors.getColor(_qrImage, com.google.android.material.R.attr.colorSurfaceContainer)
: Color.WHITE;
Bitmap bitmap;
try {
bitmap = QrCodeHelper.encodeToBitmap(selectedEntry.getUri().toString(), _qrImage.getWidth(), _qrImage.getWidth(), backgroundColor);
} catch (WriterException | GoogleAuthInfoException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}
_qrImage.setImageBitmap(bitmap);
}
}
================================================
FILE: app/src/main/java/com/beemdevelopment/aegis/ui/components/DropdownCheckBoxes.java
================================================
package com.beemdevelopment.aegis.ui.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.InputType;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.PluralsRes;
import androidx.appcompat.widget.AppCompatAutoCompleteTextView;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class DropdownCheckBoxes