Rename package to com.beemdevelopment.aegis

This commit is contained in:
Alexander Bakker 2019-02-07 22:39:33 +01:00
parent ceb03de240
commit 62425511a1
101 changed files with 366 additions and 362 deletions

View file

@ -0,0 +1,60 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.ui.MainActivity;
import androidx.annotation.RequiresApi;
import java.util.Collections;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private Preferences _prefs;
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_prefs = new Preferences(this);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
}
public DatabaseManager getDatabaseManager() {
return _manager;
}
public Preferences getPreferences() {
return _prefs;
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setIcon(Icon.createWithResource(this, R.drawable.qr_scanner))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
}

View file

@ -0,0 +1,37 @@
package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
public class Preferences {
private SharedPreferences _prefs;
public Preferences(Context context) {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public boolean isDarkModeEnabled() {
return _prefs.getBoolean("pref_dark_mode", false);
}
public boolean isSecureScreenEnabled() {
return _prefs.getBoolean("pref_secure_screen", true);
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
public void setIntroDone(boolean done) {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
}

View file

@ -0,0 +1,46 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
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, HexException {
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;
}
}

View file

@ -0,0 +1,19 @@
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;
}
}

View file

@ -0,0 +1,128 @@
package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
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.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.spongycastle.crypto.generators.SCrypt;
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 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(char[] password, SCryptParameters params) {
byte[] bytes = toBytes(password);
byte[] keyBytes = SCrypt.generate(bytes, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
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;
// apparently kitkat doesn't support GCMParameterSpec
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
spec = new IvParameterSpec(nonce);
} else {
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 {
// append the tag to the ciphertext
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(encrypted);
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;
}
private static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);
return byteBuf.array();
}
}

View file

@ -0,0 +1,106 @@
package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
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.UnrecoverableKeyException;
import java.security.cert.CertificateException;
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 {
if (!isSupported()) {
throw new KeyStoreHandleException("Symmetric KeyStore keys are not supported in this version of Android");
}
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 (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);
}
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
if (isSupported()) {
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
return null;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
return key;
}
public void deleteKey(String id) throws KeyStoreHandleException {
try {
_keyStore.deleteEntry(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.crypto;
public class KeyStoreHandleException extends Exception {
public KeyStoreHandleException(Throwable cause) {
super(cause);
}
public KeyStoreHandleException(String message) {
super(message);
}
}

View file

@ -0,0 +1,61 @@
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 NullPointerException();
}
_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();
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.crypto;
public class MasterKeyException extends Exception {
public MasterKeyException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,33 @@
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;
}
}

View file

@ -0,0 +1,189 @@
package com.beemdevelopment.aegis.crypto.otp;
/*
* OneTimePasswordAlgorithm.java
* OATH Initiative,
* HOTP one-time password algorithm
*
*/
/* Copyright (C) 2004, OATH. All rights reserved.
*
* License to copy and use this software is granted provided that it
* is identified as the "OATH HOTP Algorithm" in all material
* mentioning or referencing this software or this function.
*
* License is also granted to make and use derivative works provided
* that such works are identified as
* "derived from OATH HOTP algorithm"
* in all material mentioning or referencing the derived work.
*
* OATH (Open AuTHentication) and its members make no
* representations concerning either the merchantability of this
* software or the suitability of this software for any particular
* purpose.
*
* It is provided "as is" without express or implied warranty
* of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
* ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
*
* These notices must be retained in any copies of any part of this
* documentation and/or software.
*/
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class contains static methods that are used to calculate the
* One-Time Password (OTP) using
* JCE to provide the HMAC-SHA-1.
*
* @author Loren Hart
* @version 1.0
*/
public class HOTP {
private HOTP() {
}
// These are used to calculate the check-sum digits.
// 0 1 2 3 4 5 6 7 8 9
private static final int[] doubleDigits =
{0, 2, 4, 6, 8, 1, 3, 5, 7, 9};
/**
* Calculates the checksum using the credit card algorithm.
* This algorithm has the advantage that it detects any single
* mistyped digit and any single transposition of
* adjacent digits.
*
* @param num the number to calculate the checksum for
* @param digits number of significant places in the number
* @return the checksum of num
*/
public static int calcChecksum(long num, int digits) {
boolean doubleDigit = true;
int total = 0;
while (0 < digits--) {
int digit = (int) (num % 10);
num /= 10;
if (doubleDigit) {
digit = doubleDigits[digit];
}
total += digit;
doubleDigit = !doubleDigit;
}
int result = total % 10;
if (result > 0) {
result = 10 - result;
}
return result;
}
/**
* This method uses the JCE to provide the HMAC-SHA-1
* algorithm.
* HMAC computes a Hashed Message Authentication Code and
* in this case SHA1 is the hash algorithm used.
*
* @param keyBytes the bytes to use for the HMAC-SHA-1 key
* @param text the message or text to be authenticated.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not a valid HMAC-SHA-1 key.
*/
public static byte[] hmac_sha1(byte[] keyBytes, byte[] text)
throws NoSuchAlgorithmException, InvalidKeyException {
// try {
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException nsae) {
hmacSha1 = Mac.getInstance("HMAC-SHA-1");
}
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmacSha1.init(macKey);
return hmacSha1.doFinal(text);
// } catch (GeneralSecurityException gse) {
// throw new UndeclaredThrowableException(gse);
// }
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
/**
* This method generates an OTP value for the given
* set of parameters.
*
* @param secret the shared secret
* @param movingFactor the counter, time, or other value that
* changes on a per use basis.
* @param codeDigits the number of digits in the OTP, not
* including the checksum, if any.
* @param addChecksum a flag that indicates if a checksum digit
* should be appended to the OTP.
* @param truncationOffset the offset into the MAC result to
* begin truncation. If this value is out of
* the range of 0 ... 15, then dynamic
* truncation will be used.
* Dynamic truncation is when the last 4
* bits of the last byte of the MAC are
* used to determine the start offset.
* @return A numeric String in base 10 that includes
* {@link codeDigits} digits plus the optional checksum
* digit if requested.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not
* a valid HMAC-SHA-1 key.
*/
static public String generateOTP(byte[] secret,
long movingFactor,
int codeDigits,
boolean addChecksum,
int truncationOffset)
throws NoSuchAlgorithmException, InvalidKeyException {
// put movingFactor value into text byte array
String result = null;
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = new byte[8];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
if ((0 <= truncationOffset) &&
(truncationOffset < (hash.length - 4))) {
offset = truncationOffset;
}
int binary =
((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
}
}

View file

@ -0,0 +1,170 @@
package com.beemdevelopment.aegis.crypto.otp;
/**
Copyright (c) 2011 IETF Trust and the persons identified as
authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, is permitted pursuant to, and subject to the license
terms contained in, the Simplified BSD License set forth in Section
4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info).
*/
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
/**
* This is an example implementation of the OATH
* TOTP algorithm.
* Visit www.openauthentication.org for more information.
*
* @author Johan Rydell, PortWise, Inc.
*/
public class TOTP {
private TOTP() {
}
/**
* This method uses the JCE to provide the crypto algorithm.
* HMAC computes a Hashed Message Authentication Code with the
* crypto hash algorithm as a parameter.
*
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
* HmacSHA512)
* @param keyBytes: the bytes to use for the HMAC key
* @param text: the message or text to be authenticated
*/
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text) {
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
}
/**
* This method converts a HEX string to Byte[]
*
* @param hex: the HEX string
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex) {
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i + 1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP256(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP512(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @param crypto: the crypto function to use
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits,
String crypto) {
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16)
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] hash = hmac_sha(crypto, key, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[returnDigits];
result = Integer.toString(otp);
while (result.length() < returnDigits) {
result = "0" + result;
}
return result;
}
}

View file

@ -0,0 +1,77 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import java.util.UUID;
public class Database {
private static final int VERSION = 1;
private DatabaseEntryList _entries = new DatabaseEntryList();
public JSONObject toJson() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
array.put(e.toJson());
}
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
try {
int ver = obj.getInt("version");
if (ver != VERSION) {
throw new DatabaseException("Unsupported version");
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
db.addEntry(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
}
return db;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void removeEntry(DatabaseEntry entry) {
_entries.remove(entry);
}
public void replaceEntry(DatabaseEntry newEntry) {
_entries.replace(newEntry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
_entries.swap(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
return _entries.getList();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
return _entries.getByUUID(uuid);
}
}

View file

@ -0,0 +1,154 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import java.util.UUID;
public class DatabaseEntry implements Serializable {
private UUID _uuid;
private String _name = "";
private String _issuer = "";
private String _group;
private OtpInfo _info;
private byte[] _icon;
private DatabaseEntry(UUID uuid, OtpInfo info) {
_uuid = uuid;
_info = info;
}
public DatabaseEntry(OtpInfo info) {
this(UUID.randomUUID(), info);
}
public DatabaseEntry(OtpInfo info, String name, String issuer) {
this(info);
setName(name);
setIssuer(issuer);
}
public DatabaseEntry(GoogleAuthInfo info) {
this(info.getOtpInfo(), info.getAccountName(), info.getIssuer());
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("type", _info.getType());
obj.put("uuid", _uuid.toString());
obj.put("name", _name);
obj.put("issuer", _issuer);
obj.put("group", _group);
obj.put("icon", _icon == null ? JSONObject.NULL : Base64.encode(_icon));
obj.put("info", _info.toJson());
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public static DatabaseEntry fromJson(JSONObject obj) throws JSONException, OtpInfoException, Base64Exception {
// if there is no uuid, generate a new one
UUID uuid;
if (!obj.has("uuid")) {
uuid = UUID.randomUUID();
} else {
uuid = UUID.fromString(obj.getString("uuid"));
}
OtpInfo info = OtpInfo.fromJson(obj.getString("type"), obj.getJSONObject("info"));
DatabaseEntry entry = new DatabaseEntry(uuid, info);
entry.setName(obj.getString("name"));
entry.setIssuer(obj.getString("issuer"));
entry.setGroup(obj.optString("group", null));
Object icon = obj.get("icon");
if (icon != JSONObject.NULL) {
entry.setIcon(Base64.decode((String) icon));
}
return entry;
}
public void resetUUID() {
_uuid = UUID.randomUUID();
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public String getIssuer() {
return _issuer;
}
public String getGroup() {
return _group;
}
public byte[] getIcon() {
return _icon;
}
public OtpInfo getInfo() {
return _info;
}
public void setName(String name) {
_name = name;
}
public void setIssuer(String issuer) {
_issuer = issuer;
}
public void setGroup(String group) {
_group = group;
}
public void setInfo(OtpInfo info) {
_info = info;
}
public void setIcon(byte[] icon) {
_icon = icon;
}
public boolean hasIcon() {
return _icon != null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof DatabaseEntry)) {
return false;
}
DatabaseEntry entry = (DatabaseEntry) o;
return getUUID().equals(entry.getUUID())
&& getName().equals(entry.getName())
&& getIssuer().equals(entry.getIssuer())
&& Objects.equals(getGroup(), entry.getGroup())
&& getInfo().equals(entry.getInfo())
&& Arrays.equals(getIcon(), entry.getIcon());
}
}

View file

@ -0,0 +1,62 @@
package com.beemdevelopment.aegis.db;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@NonNull
@Override
public Iterator<DatabaseEntry> iterator() {
return _entries.iterator();
}
public void add(DatabaseEntry entry) {
if (getByUUID(entry.getUUID()) != null) {
throw new AssertionError("entry found with the same uuid");
}
_entries.add(entry);
}
public void remove(DatabaseEntry entry) {
entry = mustGetByUUID(entry.getUUID());
_entries.remove(entry);
}
public void replace(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
_entries.set(_entries.indexOf(oldEntry), newEntry);
}
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
}
public List<DatabaseEntry> getList() {
return Collections.unmodifiableList(_entries);
}
public DatabaseEntry getByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
private DatabaseEntry mustGetByUUID(UUID uuid) {
DatabaseEntry entry = getByUUID(uuid);
if (entry == null) {
throw new AssertionError("no entry found with the same uuid");
}
return entry;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db;
public class DatabaseException extends Exception {
public DatabaseException(Throwable cause) {
super(cause);
}
public DatabaseException(String message) {
super(message);
}
}

View file

@ -0,0 +1,167 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotListException;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
public class DatabaseFile {
public static final byte VERSION = 1;
private Object _content;
private Header _header;
public DatabaseFile() {
}
private DatabaseFile(Object content, Header header) {
_content = content;
_header = header;
}
public Header getHeader() {
return _header;
}
public boolean isEncrypted() {
return !_header.isEmpty();
}
public JSONObject toJson() {
try {
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("header", _header.toJson());
obj.put("db", _content);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public byte[] toBytes() {
JSONObject obj = toJson();
try {
String string = obj.toString(4);
return string.getBytes("UTF-8");
} catch (JSONException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public static DatabaseFile fromJson(JSONObject obj) throws DatabaseFileException {
try {
if (obj.getInt("version") > VERSION) {
throw new DatabaseFileException("unsupported version");
}
Header header = Header.fromJson(obj.getJSONObject("header"));
if (!header.isEmpty()) {
return new DatabaseFile(obj.getString("db"), header);
}
return new DatabaseFile(obj.getJSONObject("db"), header);
} catch (JSONException e) {
throw new DatabaseFileException(e);
}
}
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
try {
JSONObject obj = new JSONObject(new String(data, "UTF-8"));
return DatabaseFile.fromJson(obj);
} catch (UnsupportedEncodingException | JSONException e) {
throw new DatabaseFileException(e);
}
}
public JSONObject getContent() {
return (JSONObject) _content;
}
public JSONObject getContent(DatabaseFileCredentials creds) throws DatabaseFileException {
try {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), "UTF-8"));
} catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
throw new DatabaseFileException(e);
}
}
public void setContent(JSONObject obj) {
_content = obj;
_header = new Header(null, null);
}
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
try {
String string = obj.toString(4);
byte[] dbBytes = string.getBytes("UTF-8");
CryptResult result = creds.encrypt(dbBytes);
_content = Base64.encode(result.getData());
_header = new Header(creds.getSlots(), result.getParams());
} catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
throw new DatabaseFileException(e);
}
}
public static class Header {
private SlotList _slots;
private CryptParameters _params;
public Header(SlotList slots, CryptParameters params) {
_slots = slots;
_params = params;
}
public static Header fromJson(JSONObject obj) throws DatabaseFileException {
if (obj.isNull("slots") && obj.isNull("params")) {
return new Header(null, null);
}
try {
SlotList slots = SlotList.fromJson(obj.getJSONArray("slots"));
CryptParameters params = CryptParameters.fromJson(obj.getJSONObject("params"));
return new Header(slots, params);
} catch (SlotListException | JSONException | HexException e) {
throw new DatabaseFileException(e);
}
}
public JSONObject toJson() {
try {
JSONObject obj = new JSONObject();
obj.put("slots", _slots != null ? _slots.toJson() : JSONObject.NULL);
obj.put("params", _params != null ? _params.toJson() : JSONObject.NULL);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public SlotList getSlots() {
return _slots;
}
public CryptParameters getParams() {
return _params;
}
public boolean isEmpty() {
return _slots == null && _params == null;
}
}
}

View file

@ -0,0 +1,41 @@
package com.beemdevelopment.aegis.db;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import java.io.Serializable;
import com.beemdevelopment.aegis.db.slots.SlotList;
public class DatabaseFileCredentials implements Serializable {
private MasterKey _key;
private SlotList _slots;
public DatabaseFileCredentials() {
_key = MasterKey.generate();
_slots = new SlotList();
}
public DatabaseFileCredentials(MasterKey key, SlotList slots) {
_key = key;
_slots = slots;
}
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
return _key.encrypt(bytes);
}
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
return _key.decrypt(bytes, params);
}
public MasterKey getKey() {
return _key;
}
public SlotList getSlots() {
return _slots;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db;
public class DatabaseFileException extends Exception {
public DatabaseFileException(Throwable cause) {
super(cause);
}
public DatabaseFileException(String message) {
super(message);
}
}

View file

@ -0,0 +1,236 @@
package com.beemdevelopment.aegis.db;
import android.content.Context;
import android.os.Environment;
import org.json.JSONObject;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Collator;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
public class DatabaseManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
private static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
private Database _db;
private DatabaseFile _file;
private DatabaseFileCredentials _creds;
private boolean _encrypt;
private Context _context;
public DatabaseManager(Context context) {
_context = context;
}
public boolean fileExists() {
File file = new File(_context.getFilesDir(), FILENAME);
return file.exists() && file.isFile();
}
public void load() throws DatabaseManagerException {
assertState(true, false);
try (FileInputStream file = _context.openFileInput(FILENAME)) {
byte[] fileBytes = new byte[(int) file.getChannel().size()];
DataInputStream stream = new DataInputStream(file);
stream.readFully(fileBytes);
stream.close();
_file = DatabaseFile.fromBytes(fileBytes);
_encrypt = _file.isEncrypted();
if (!isEncryptionEnabled()) {
JSONObject obj = _file.getContent();
_db = Database.fromJson(obj);
}
} catch (IOException | DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public void lock() {
assertState(false, true);
_creds = null;
_db = null;
}
public void unlock(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(true, true);
try {
JSONObject obj = _file.getContent(creds);
_db = Database.fromJson(obj);
_creds = creds;
} catch (DatabaseFileException | DatabaseException e) {
throw new DatabaseManagerException(e);
}
}
public static void save(Context context, DatabaseFile file) throws DatabaseManagerException {
byte[] bytes = file.toBytes();
try (FileOutputStream stream = context.openFileOutput(FILENAME, Context.MODE_PRIVATE)) {
stream.write(bytes);
} catch (IOException e) {
throw new DatabaseManagerException(e);
}
}
public void save() throws DatabaseManagerException {
assertState(false, true);
try {
JSONObject obj = _db.toJson();
if (isEncryptionEnabled()) {
_file.setContent(obj, _creds);
} else {
_file.setContent(obj);
}
save(_context, _file);
} catch (DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public String export(boolean encrypt) throws DatabaseManagerException {
assertState(false, true);
try {
DatabaseFile dbFile = new DatabaseFile();
if (encrypt && isEncryptionEnabled()) {
dbFile.setContent(_db.toJson(), _creds);
} else {
dbFile.setContent(_db.toJson());
}
String dirName = !BuildConfig.DEBUG ? _context.getString(R.string.app_name) : _context.getString(R.string.app_name_dev);
File dir = new File(Environment.getExternalStorageDirectory(), dirName);
if (!dir.exists() && !dir.mkdirs()) {
throw new IOException("error creating external storage directory");
}
byte[] bytes = dbFile.toBytes();
File file = new File(dir.getAbsolutePath(), encrypt ? FILENAME_EXPORT : FILENAME_EXPORT_PLAIN);
try (FileOutputStream stream = new FileOutputStream(file)) {
stream.write(bytes);
}
return file.getAbsolutePath();
} catch (IOException | DatabaseFileException e) {
throw new DatabaseManagerException(e);
}
}
public void addEntry(DatabaseEntry entry) {
assertState(false, true);
_db.addEntry(entry);
}
public void removeEntry(DatabaseEntry entry) {
assertState(false, true);
_db.removeEntry(entry);
}
public void replaceEntry(DatabaseEntry entry) {
assertState(false, true);
_db.replaceEntry(entry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
assertState(false, true);
_db.swapEntries(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
assertState(false, true);
return _db.getEntries();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
assertState(false, true);
return _db.getEntryByUUID(uuid);
}
public TreeSet<String> getGroups() {
assertState(false, true);
TreeSet<String> groups = new TreeSet<>(Collator.getInstance());
for (DatabaseEntry entry : getEntries()) {
String group = entry.getGroup();
if (group != null) {
groups.add(group);
}
}
return groups;
}
public DatabaseFileCredentials getCredentials() {
assertState(false, true);
return _creds;
}
public void setCredentials(DatabaseFileCredentials creds) {
assertState(false, true);
_creds = creds;
}
public DatabaseFile.Header getFileHeader() {
assertLoaded(true);
return _file.getHeader();
}
public boolean isEncryptionEnabled() {
assertLoaded(true);
return _encrypt;
}
public void enableEncryption(DatabaseFileCredentials creds) throws DatabaseManagerException {
assertState(false, true);
_creds = creds;
_encrypt = true;
save();
}
public void disableEncryption() throws DatabaseManagerException {
assertState(false, true);
_creds = null;
_encrypt = false;
save();
}
public boolean isLoaded() {
return _file != null;
}
public boolean isLocked() {
return _db == null;
}
private void assertState(boolean locked, boolean loaded) {
assertLoaded(loaded);
if (isLocked() && !locked) {
throw new AssertionError("database file has not been unlocked yet");
} else if (!isLocked() && locked) {
throw new AssertionError("database file has already been unlocked");
}
}
private void assertLoaded(boolean loaded) {
if (isLoaded() && !loaded) {
throw new AssertionError("database file has already been loaded");
} else if (!isLoaded() && loaded) {
throw new AssertionError("database file has not been loaded yet");
}
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.db;
public class DatabaseManagerException extends Exception {
public DatabaseManagerException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,20 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
super();
}
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_FINGERPRINT;
}
}

View file

@ -0,0 +1,55 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.Hex;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.UUID;
import javax.crypto.SecretKey;
public class PasswordSlot extends RawSlot {
private SCryptParameters _params;
public PasswordSlot() {
super();
}
protected PasswordSlot(UUID uuid, byte[] key, CryptParameters keyParams, SCryptParameters scryptParams) {
super(uuid, key, keyParams);
_params = scryptParams;
}
@Override
public JSONObject toJson() {
try {
JSONObject obj = super.toJson();
obj.put("n", _params.getN());
obj.put("r", _params.getR());
obj.put("p", _params.getP());
obj.put("salt", Hex.encode(_params.getSalt()));
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public SecretKey deriveKey(char[] password, SCryptParameters params) {
SecretKey key = CryptoUtils.deriveKey(password, params);
_params = params;
return key;
}
public SecretKey deriveKey(char[] password) {
return CryptoUtils.deriveKey(password, _params);
}
@Override
public byte getType() {
return TYPE_DERIVED;
}
}

View file

@ -0,0 +1,20 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class RawSlot extends Slot {
public RawSlot() {
super();
}
protected RawSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_RAW;
}
}

View file

@ -0,0 +1,152 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.Serializable;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
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 abstract class Slot implements Serializable {
public final static byte TYPE_RAW = 0x00;
public final static byte TYPE_DERIVED = 0x01;
public final static byte TYPE_FINGERPRINT = 0x02;
private UUID _uuid;
private byte[] _encryptedMasterKey;
private CryptParameters _encryptedMasterKeyParams;
protected Slot() {
_uuid = UUID.randomUUID();
}
protected Slot(UUID uuid, byte[] key, CryptParameters keyParams) {
_uuid = uuid;
_encryptedMasterKey = key;
_encryptedMasterKeyParams = keyParams;
}
// getKey decrypts the encrypted master key in this slot using the given cipher and returns it.
public MasterKey getKey(Cipher cipher) throws SlotException, SlotIntegrityException {
try {
CryptResult res = CryptoUtils.decrypt(_encryptedMasterKey, cipher, _encryptedMasterKeyParams);
SecretKey key = new SecretKeySpec(res.getData(), CryptoUtils.CRYPTO_AEAD);
return new MasterKey(key);
} catch (BadPaddingException e) {
throw new SlotIntegrityException(e);
} catch (IOException | IllegalBlockSizeException e) {
throw new SlotException(e);
}
}
// setKey encrypts the given master key using the given cipher and stores the result in this slot.
public void setKey(MasterKey masterKey, Cipher cipher) throws SlotException {
try {
byte[] masterKeyBytes = masterKey.getBytes();
CryptResult res = CryptoUtils.encrypt(masterKeyBytes, cipher);
_encryptedMasterKey = res.getData();
_encryptedMasterKeyParams = res.getParams();
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new SlotException(e);
}
}
public static Cipher createEncryptCipher(SecretKey key) throws SlotException {
try {
return CryptoUtils.createEncryptCipher(key);
} catch (InvalidAlgorithmParameterException
| NoSuchPaddingException
| NoSuchAlgorithmException
| InvalidKeyException e) {
throw new SlotException(e);
}
}
public Cipher createDecryptCipher(SecretKey key) throws SlotException {
try {
return CryptoUtils.createDecryptCipher(key, _encryptedMasterKeyParams.getNonce());
} catch (InvalidAlgorithmParameterException
| NoSuchAlgorithmException
| InvalidKeyException
| NoSuchPaddingException e) {
throw new SlotException(e);
}
}
public JSONObject toJson() {
try {
JSONObject obj = new JSONObject();
obj.put("type", getType());
obj.put("uuid", _uuid.toString());
obj.put("key", Hex.encode(_encryptedMasterKey));
obj.put("key_params", _encryptedMasterKeyParams.toJson());
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Slot fromJson(JSONObject obj) throws SlotException {
Slot slot;
try {
UUID uuid;
if (!obj.has("uuid")) {
uuid = UUID.randomUUID();
} else {
uuid = UUID.fromString(obj.getString("uuid"));
}
byte[] key = Hex.decode(obj.getString("key"));
CryptParameters keyParams = CryptParameters.fromJson(obj.getJSONObject("key_params"));
switch (obj.getInt("type")) {
case Slot.TYPE_RAW:
slot = new RawSlot(uuid, key, keyParams);
break;
case Slot.TYPE_DERIVED:
SCryptParameters scryptParams = new SCryptParameters(
obj.getInt("n"),
obj.getInt("r"),
obj.getInt("p"),
Hex.decode(obj.getString("salt"))
);
slot = new PasswordSlot(uuid, key, keyParams, scryptParams);
break;
case Slot.TYPE_FINGERPRINT:
slot = new FingerprintSlot(uuid, key, keyParams);
break;
default:
throw new SlotException("unrecognized slot type");
}
} catch (JSONException | HexException e) {
throw new SlotException(e);
}
return slot;
}
public abstract byte getType();
public UUID getUUID() {
return _uuid;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db.slots;
public class SlotException extends Exception {
public SlotException(Throwable cause) {
super(cause);
}
public SlotException(String message) {
super(message);
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db.slots;
public class SlotIntegrityException extends Exception {
public SlotIntegrityException() {
}
public SlotIntegrityException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,84 @@
package com.beemdevelopment.aegis.db.slots;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SlotList implements Iterable<Slot>, Serializable {
private List<Slot> _slots = new ArrayList<>();
public JSONArray toJson() {
JSONArray array = new JSONArray();
for (Slot slot : this) {
array.put(slot.toJson());
}
return array;
}
public static SlotList fromJson(JSONArray array) throws SlotListException {
SlotList slots = new SlotList();
try {
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
Slot slot = Slot.fromJson(obj);
slots.add(slot);
}
} catch (SlotException | JSONException e) {
throw new SlotListException(e);
}
return slots;
}
public void add(Slot slot) {
for (Slot s : this) {
if (s.getUUID().equals(slot.getUUID())) {
throw new AssertionError("slot found with the same uuid");
}
}
_slots.add(slot);
}
public void remove(Slot slot) {
_slots.remove(slot);
}
public int size() {
return _slots.size();
}
public <T extends Slot> T find(Class<T> type) {
for (Slot slot : this) {
if (slot.getClass() == type) {
return type.cast(slot);
}
}
return null;
}
public <T extends Slot> List<T> findAll(Class<T> type) {
ArrayList<T> list = new ArrayList<>();
for (Slot slot : this) {
if (slot.getClass() == type) {
list.add(type.cast(slot));
}
}
return list;
}
public <T extends Slot> boolean has(Class<T> type) {
return find(type) != null;
}
@Override
public Iterator<Slot> iterator() {
return _slots.iterator();
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.db.slots;
public class SlotListException extends Exception {
public SlotListException(Throwable cause) {
super(cause);
}
public SlotListException(String message) {
super(message);
}
}

View file

@ -0,0 +1,144 @@
package com.beemdevelopment.aegis.encoding;
// modified for use in Aegis
/* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info.
*
* As modified by Patrick Woodworth:
*
* Copyright 2011 Patrick Woodworth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
/**
* Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html )
*
* @author Robert Kaye
* @author Gordon Mohr
*/
public class Base32 {
private static final String base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static final int[] base32Lookup =
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
};
/**
* Encodes byte array to Base32 String.
*
* @param bytes Bytes to encode.
* @return Encoded byte array <code>bytes</code> as a String.
*
*/
public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte;
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
/* Is the current digit going to span a byte boundary? */
if (index > 3) {
if ((i + 1) < bytes.length) {
nextByte =
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
} else {
nextByte = 0;
}
digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= nextByte >> (8 - index);
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;
if (index == 0)
i++;
}
base32[j++] = base32Chars.charAt(digit);
}
return Arrays.copyOf(base32, j);
}
/**
* Decodes the given Base32 String to a raw byte array.
*
* @param base32
* @return Decoded <code>base32</code> String as a raw byte array.
*/
public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
lookup = base32[i] - '0';
digit = decodeDigit(lookup);
if (index <= 3) {
index = (index + 5) % 8;
if (index == 0) {
bytes[offset] |= digit;
offset++;
if (offset >= bytes.length)
break;
} else {
bytes[offset] |= digit << (8 - index);
}
} else {
index = (index + 5) % 8;
bytes[offset] |= (digit >>> index);
offset++;
if (offset >= bytes.length) {
break;
}
bytes[offset] |= digit << (8 - index);
}
}
return bytes;
}
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}

View file

@ -0,0 +1,29 @@
package com.beemdevelopment.aegis.encoding;
import java.io.UnsupportedEncodingException;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
private Base64() {
}
public static byte[] decode(String s) throws Base64Exception {
try {
return android.util.Base64.decode(s, _flags);
} catch (IllegalArgumentException e) {
throw new Base64Exception(e);
}
}
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
try {
return new String(encoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class Base64Exception extends Exception {
public Base64Exception(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,46 @@
package com.beemdevelopment.aegis.encoding;
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
// It is licensed under GPLv2 with a classpath exception.
public class Hex {
private Hex() {
}
private static int hexToBin(char ch) {
if ('0' <= ch && ch <= '9') return ch - '0';
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
return -1;
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static byte[] decode(String s) throws HexException {
final int len = s.length();
if (len % 2 != 0)
throw new HexException("hexBinary needs to be even-length: " + s);
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int h = hexToBin(s.charAt(i));
int l = hexToBin(s.charAt(i + 1));
if (h == -1 || l == -1)
throw new HexException("contains illegal character for hexBinary: " + s);
out[i / 2] = (byte) (h * 16 + l);
}
return out;
}
public static String encode(byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}

View file

@ -0,0 +1,28 @@
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.widget.EditText;
import java.util.Arrays;
public class EditTextHelper {
private EditTextHelper() {
}
public static void clearEditText(EditText text) {
text.getText().clear();
}
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);
}
}

View file

@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.helpers;
import android.Manifest;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class FingerprintHelper {
private FingerprintHelper() {
}
public static FingerprintManager getManager(Context context) {
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}

View file

@ -0,0 +1,156 @@
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
// It has been modified to suit Aegis' needs
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import androidx.annotation.RequiresApi;
import android.widget.TextView;
import com.mattprecious.swirl.SwirlView;
import com.beemdevelopment.aegis.R;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 100;
private final FingerprintManager mFingerprintManager;
private final SwirlView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
private boolean mSelfCancelled;
/**
* Constructor for {@link FingerprintUiHelper}.
*/
public FingerprintUiHelper(FingerprintManager fingerprintManager,
SwirlView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setState(SwirlView.State.ON);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setState(SwirlView.State.OFF);
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mIcon.setState(SwirlView.State.ON);
}
}, 500);
}
private void showError(CharSequence error) {
mIcon.setState(SwirlView.State.ERROR);
mErrorTextView.setText(error);
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setState(SwirlView.State.ON);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}

View file

@ -0,0 +1,39 @@
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 <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* 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.<br/>
* <br/>
* 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);
}

View file

@ -0,0 +1,45 @@
package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
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<String> 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;
}
}

View file

@ -0,0 +1,59 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
_adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return _isLongPressDragEnabled;
}
public void setIsLongPressDragEnabled(boolean enabled) {
_isLongPressDragEnabled = enabled;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
_adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
_positionChanged = true;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getAdapterPosition());
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getAdapterPosition());
_positionChanged = false;
}
}
}

View file

@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.annotation.ArrayRes;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
public class SpinnerHelper {
private SpinnerHelper() {
}
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
initSpinner(spinner, adapter);
}
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
initSpinner(spinner, adapter);
}
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.invalidate();
}
}

View file

@ -0,0 +1,29 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import me.dm7.barcodescanner.core.ViewFinderView;
public class SquareFinderView extends ViewFinderView {
public SquareFinderView(Context context) {
super(context);
init();
}
public SquareFinderView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setSquareViewFinder(true);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}

View file

@ -0,0 +1,53 @@
package com.beemdevelopment.aegis.helpers;
import android.view.View;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
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()
.buildRect(text.substring(0, 1).toUpperCase(), color);
}
}

View file

@ -0,0 +1,41 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
public class UiRefresher {
private boolean _running;
private Listener _listener;
private Handler _handler;
public UiRefresher(Listener listener) {
_listener = listener;
_handler = new Handler();
}
public void start() {
if (_running) {
return;
}
_running = true;
_listener.onRefresh();
_handler.postDelayed(new Runnable() {
@Override
public void run() {
if (_running) {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}
}, _listener.getMillisTillNextRefresh());
}
public void stop() {
_running = false;
}
public interface Listener {
void onRefresh();
long getMillisTillNextRefresh();
}
}

View file

@ -0,0 +1,62 @@
package com.beemdevelopment.aegis.importers;
import org.json.JSONObject;
import java.util.List;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseException;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisImporter(ByteInputStream stream) {
super(stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
Database db = Database.fromJson(obj);
return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}

View file

@ -0,0 +1,85 @@
package com.beemdevelopment.aegis.importers;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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.ByteInputStream;
public class AndOtpImporter extends DatabaseImporter {
private JSONArray _obj;
public AndOtpImporter(ByteInputStream stream) {
super(stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = "";
String name = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
} catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
}

View file

@ -0,0 +1,47 @@
package com.beemdevelopment.aegis.importers;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
public abstract class DatabaseImporter {
private static Map<String, Class<? extends DatabaseImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisImporter.class);
importers.put("andOTP", AndOtpImporter.class);
importers.put("FreeOTP", FreeOtpImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
protected ByteInputStream _stream;
protected DatabaseImporter(ByteInputStream stream) {
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public static DatabaseImporter create(ByteInputStream stream, Class<? extends DatabaseImporter> type) {
try {
return type.getConstructor(ByteInputStream.class).newInstance(stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return _importers;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterException extends Exception {
public DatabaseImporterException(Throwable cause) {
super(cause);
}
public DatabaseImporterException(String message) {
super(message);
}
}

View file

@ -0,0 +1,160 @@
package com.beemdevelopment.aegis.importers;
import android.util.Xml;
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.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
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.ByteInputStream;
public class FreeOtpImporter extends DatabaseImporter {
private List<XmlEntry> _xmlEntries;
public FreeOtpImporter(ByteInputStream stream) {
super(stream);
}
private static class XmlEntry {
String Name;
String Value;
}
@Override
public void parse() throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.nextTag();
_xmlEntries = parse(parser);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (XmlEntry xmlEntry : _xmlEntries) {
if (xmlEntry.Name.equals("tokenOrder")) {
// TODO: order
JSONArray array = new JSONArray(xmlEntry.Value);
} else {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
}
} catch (OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
entries.add(parseEntry(parser));
}
return entries;
}
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 XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
XmlEntry entry = new XmlEntry();
entry.Name = name;
entry.Value = value;
return entry;
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}

View file

@ -0,0 +1,157 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
public class GoogleAuthInfo {
private OtpInfo _info;
private String _accountName;
private String _issuer;
public GoogleAuthInfo(OtpInfo info, String accountName, String issuer) {
_info = info;
_accountName = accountName;
_issuer = issuer;
}
public OtpInfo getOtpInfo() {
return _info;
}
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
if (_info instanceof TotpInfo) {
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();
}
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
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 static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("bad uri format");
}
return GoogleAuthInfo.parseUri(uri);
}
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
if (!uri.getScheme().equals("otpauth")) {
throw new GoogleAuthInfoException("unsupported protocol");
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException("'secret' is not set");
}
// decode secret
byte[] secret;
try {
secret = Base32.decode(encodedSecret.toCharArray());
} catch (Base32Exception e) {
throw new GoogleAuthInfoException("bad secret", e);
}
// check the otp type
OtpInfo info;
try {
String type = uri.getHost();
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 "hotp":
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("'counter' was not set");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
default:
throw new GoogleAuthInfoException(String.format("unsupported otp type: %s", type));
}
} catch (OtpInfoException e) {
throw new GoogleAuthInfoException(e);
}
// provider info used to disambiguate accounts
String path = uri.getPath();
String label = path != null && path.length() > 0 ? path.substring(1) : "";
String accountName = "";
String issuer = "";
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");
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 e) {
throw new GoogleAuthInfoException(e);
}
return new GoogleAuthInfo(info, accountName, issuer);
}
public String getIssuer() {
return _issuer;
}
public String getAccountName() {
return _accountName;
}
}

View file

@ -0,0 +1,24 @@
package com.beemdevelopment.aegis.otp;
public class GoogleAuthInfoException extends Exception {
public GoogleAuthInfoException(Throwable cause) {
super(cause);
}
public GoogleAuthInfoException(String message) {
super(message);
}
public GoogleAuthInfoException(String message, Throwable cause) {
super(message, cause);
}
@Override
public String getMessage() {
Throwable cause = getCause();
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}

View file

@ -0,0 +1,77 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
private long _counter;
public HotpInfo(byte[] secret) throws OtpInfoException {
super(secret);
setCounter(0);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
super(secret, algorithm, digits);
setCounter(counter);
}
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}
}
@Override
public String getType() {
return "hotp";
}
@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();
}
}

View file

@ -0,0 +1,132 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.Arrays;
public abstract class OtpInfo implements Serializable {
private byte[] _secret;
private String _algorithm;
private int _digits;
public OtpInfo(byte[] secret) throws OtpInfoException {
this(secret, "SHA1", 6);
}
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
setSecret(secret);
setAlgorithm(algorithm);
setDigits(digits);
}
public abstract String getOtp();
public abstract String getType();
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("secret", new String(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");
}
public void setAlgorithm(String algorithm) throws OtpInfoException {
if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4);
}
algorithm = algorithm.toUpperCase();
if (!isAlgorithmValid(algorithm)) {
throw new OtpInfoException(String.format("unsupported algorithm: %s", algorithm));
}
_algorithm = algorithm;
}
public static boolean isDigitsValid(int digits) {
return digits == 6 || digits == 8;
}
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").toCharArray());
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
switch (type) {
case "totp":
int period = obj.getInt("period");
info = new TotpInfo(secret, algo, digits, period);
break;
case "hotp":
long counter = obj.getLong("counter");
info = new HotpInfo(secret, algo, digits, counter);
break;
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
} catch (Base32Exception | 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 Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.otp;
public class OtpInfoException extends Exception {
public OtpInfoException(Throwable cause) {
super(cause);
}
public OtpInfoException(String message) {
super(message);
}
}

View file

@ -0,0 +1,76 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
public class TotpInfo extends OtpInfo {
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
super(secret);
setPeriod(30);
}
public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
super(secret, algorithm, digits);
setPeriod(period);
}
@Override
public String getOtp() {
String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod());
return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true));
}
@Override
public String getType() {
return "totp";
}
@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) {
return period > 0;
}
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();
}
}

View file

@ -0,0 +1,43 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.WindowManager;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
public abstract class AegisActivity extends AppCompatActivity {
private AegisApplication _app;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set FLAG_SECURE on the window of every AegisActivity
if (getPreferences().isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// set the theme
setPreferredTheme(getPreferences().isDarkModeEnabled());
}
protected AegisApplication getApp() {
return _app;
}
protected Preferences getPreferences() {
return _app.getPreferences();
}
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
}
}
}

View file

@ -0,0 +1,179 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import androidx.appcompat.app.AlertDialog;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
private EditText _textPassword;
private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private Cipher _fingerCipher;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
Button decryptButton = findViewById(R.id.button_decrypt);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
decryptButton.performClick();
}
return false;
});
SwirlView imgFingerprint = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert);
imgFingerprint = new SwirlView(this);
insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
boolean invalidated = false;
try {
// find a fingerprint slot with an id that matches an alias in the keystore
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.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;
}
_fingerCipher = slot.createDecryptCipher(key);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (KeyStoreHandleException | SlotException e) {
throw new UndeclaredThrowableException(e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxFingerprintInfo.setVisibility(View.VISIBLE);
}
}
decryptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
});
}
private void showError() {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
}
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
new SlotListTask<>(type, this, this).execute(params);
}
private void setKey(MasterKey key) {
// send the master key back to the main activity
Intent result = new Intent();
result.putExtra("creds", new DatabaseFileCredentials(key, _slots));
setResult(RESULT_OK, result);
finish();
}
@Override
public void onBackPressed() {
// ignore back button presses
}
@Override
public void onResume() {
super.onResume();
if (_fingerHelper != null) {
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
}
}
@Override
public void onPause() {
super.onPause();
if (_fingerHelper != null) {
_fingerHelper.stopListening();
}
}
@Override
public void onAuthenticated() {
trySlots(FingerprintSlot.class, _fingerCipher);
}
@Override
public void onError() {
}
@Override
public void onTaskFinished(MasterKey key) {
if (key != null) {
setKey(key);
} else {
showError();
}
}
}

View file

@ -0,0 +1,207 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.mattprecious.swirl.SwirlView;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
public class Dialogs {
private Dialogs() {
}
public static void secureDialog(Dialog dialog) {
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
public static void showSecureDialog(Dialog dialog) {
if (new Preferences(dialog.getContext()).isSecureScreenEnabled()) {
secureDialog(dialog);
}
dialog.show();
}
public static void showTextInputDialog(Context context, @StringRes int titleId, TextInputListener listener) {
EditText input = new EditText(context);
showSecureDialog(new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(input)
.setPositiveButton(android.R.string.ok, (dialog, which) ->
listener.onTextInputResult(input.getText().toString()))
.create());
}
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
.setMessage(activity.getString(R.string.delete_entry_description))
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
}
public static void showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.discard_changes))
.setMessage(activity.getString(R.string.discard_changes_description))
.setPositiveButton(R.string.save, onSave)
.setNegativeButton(R.string.discard, onDiscard)
.create());
}
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setEnabled(false);
buttonOK.set(button);
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
return;
}
char[] password = EditTextHelper.getEditTextChars(textPassword);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(activity, key -> {
Cipher cipher;
try {
cipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
listener.onException(e);
dialog.cancel();
return;
}
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params(slot, password));
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
public void afterTextChanged(Editable c) { }
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
showSecureDialog(dialog);
}
public static void showCreateNewGroupDialog(Activity activity, TextInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_newgroup, null);
EditText groupName = view.findViewById(R.id.text_groupname);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_group)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onTextInputResult(groupName.getText().toString()))
.create();
showSecureDialog(dialog);
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
Cipher cipher;
FingerprintSlot slot;
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
FingerprintManager manager = FingerprintHelper.getManager(activity);
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.register_fingerprint)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(d -> {
helper.get().stopListening();
})
.create();
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
}
@Override
public void onError() {
}
}));
helper.get().startListening(new FingerprintManager.CryptoObject(cipher));
showSecureDialog(dialog);
}
public interface TextInputListener {
void onTextInputResult(String text);
}
public interface SlotListener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
}

View file

@ -0,0 +1,608 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.ArrayRes;
import androidx.appcompat.app.ActionBar;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
private boolean _isNew = false;
private DatabaseEntry _origEntry;
private TreeSet<String> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
private boolean _hasChangedIcon = false;
private CircleImageView _iconView;
private ImageView _saveImageButton;
private EditText _textName;
private EditText _textIssuer;
private EditText _textPeriod;
private EditText _textCounter;
private EditText _textSecret;
private TableRow _rowPeriod;
private TableRow _rowCounter;
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerDigits;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
private KropView _kropView;
private RelativeLayout _advancedSettingsHeader;
private RelativeLayout _advancedSettings;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_entry);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
// retrieve info from the calling activity
Intent intent = getIntent();
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
if (_isNew) {
setTitle(R.string.add_new_profile);
}
// 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);
_textPeriod = findViewById(R.id.text_period);
_rowPeriod = findViewById(R.id.row_period);
_textCounter = findViewById(R.id.text_counter);
_rowCounter = findViewById(R.id.row_counter);
_textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry != null) {
if (_origEntry.hasIcon()) {
byte[] imageBytes = _origEntry.getIcon();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_iconView.setImageBitmap(bitmap);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String digits = Integer.toString(_origEntry.getInfo().getDigits());
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
}
// update the icon if the text changed
_textIssuer.addTextChangedListener(_iconChangeListener);
_textName.addTextChangedListener(_iconChangeListener);
// show/hide period and counter fields on type change
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String type = _spinnerType.getSelectedItem().toString();
switch (type.toLowerCase()) {
case "totp":
_rowCounter.setVisibility(View.GONE);
_rowPeriod.setVisibility(View.VISIBLE);
break;
case "hotp":
_rowPeriod.setVisibility(View.GONE);
_rowCounter.setVisibility(View.VISIBLE);
break;
default:
throw new RuntimeException();
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
final Activity activity = this;
_spinnerGroup.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
private int prevPosition;
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position == _spinnerGroupList.size() - 1) {
Dialogs.showCreateNewGroupDialog(activity, text -> {
if (text.isEmpty()) {
return;
}
_groups.add(text);
// reset the selection to "No group" to work around a quirk
_spinnerGroup.setSelection(0, false);
updateGroupSpinnerList();
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(text), false);
});
_spinnerGroup.setSelection(prevPosition, false);
} else {
prevPosition = position;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
_iconView.setOnClickListener(v -> {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
});
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
}
}
@Override
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
}
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
fadeOut.setDuration(220);
_advancedSettingsHeader.startAnimation(fadeOut);
Animation fadeIn = new AlphaAnimation(0, 1);
fadeIn.setInterpolator(new AccelerateInterpolator());
fadeIn.setDuration(250);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
_advancedSettingsHeader.setVisibility(View.GONE);
_advancedSettings.startAnimation(fadeIn);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
fadeIn.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
_advancedSettings.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
private void updateGroupSpinnerList() {
Resources res = getResources();
_spinnerGroupList.clear();
_spinnerGroupList.add(res.getString(R.string.no_group));
_spinnerGroupList.addAll(_groups);
_spinnerGroupList.add(res.getString(R.string.new_group));
}
@Override
public void onBackPressed() {
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<DatabaseEntry> entry = new AtomicReference<>();
try {
entry.set(parseEntry());
} catch (ParseException e) {
msg.set(e.getMessage());
}
// close the activity if the entry has not been changed
if (_origEntry != null && !_hasChangedIcon && _origEntry.equals(entry.get())) {
super.onBackPressed();
return;
}
// ask for confirmation if the entry has been changed
Dialogs.showDiscardDialog(this,
(dialog, which) -> {
// if the entry couldn't be parsed, we show an error dialog
if (msg.get() != null) {
onSaveError(msg.get());
return;
}
finish(entry.get(), false);
},
(dialog, which) -> super.onBackPressed()
);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_save:
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
finish(_origEntry, true);
});
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_hasCustomIcon = false;
_hasChangedIcon = true;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@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 finish(DatabaseEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entry", entry);
intent.putExtra("delete", delete);
setResult(RESULT_OK, intent);
finish();
}
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Uri inputFile = (data.getData());
InputStream inputStream;
Bitmap bitmap;
try {
inputStream = getContentResolver().openInputStream(inputFile);
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
_kropView.setBitmap(bitmap);
_kropView.setVisibility(View.VISIBLE);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
_saveImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
_kropView.setVisibility(View.GONE);
_hasCustomIcon = true;
_hasChangedIcon = true;
}
});
}
super.onActivityResult(requestCode, resultCode, data);
}
private DatabaseEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
}
String type = _spinnerType.getSelectedItem().toString();
String algo = _spinnerAlgo.getSelectedItem().toString();
int digits;
try {
digits = Integer.parseInt(_spinnerDigits.getSelectedItem().toString());
} catch (NumberFormatException e) {
throw new ParseException("Digits is not an integer.");
}
byte[] secret;
try {
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
} catch (Base32Exception e) {
throw new ParseException("Secret is not valid base32.");
}
// set otp info
OtpInfo info;
try {
switch (type.toLowerCase()) {
case "totp":
int period;
try {
period = Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
info = new TotpInfo(secret, algo, digits, period);
break;
case "hotp":
long counter;
try {
counter = Long.parseLong(_textCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Counter is not an integer.");
}
info = new HotpInfo(secret, algo, digits, counter);
break;
default:
throw new RuntimeException();
}
info.setDigits(digits);
info.setAlgorithm(algo);
} catch (OtpInfoException e) {
throw new ParseException("The entered info is incorrect: " + e.getMessage());
}
// set database entry info
DatabaseEntry entry;
if (_origEntry == null) {
entry = new DatabaseEntry(info);
} else {
entry = cloneEntry(_origEntry);
entry.setInfo(info);
}
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
int groupPos = _spinnerGroup.getSelectedItemPosition();
if (groupPos != 0) {
String group = _spinnerGroupList.get(_spinnerGroup.getSelectedItemPosition());
entry.setGroup(group);
} else {
entry.setGroup(null);
}
if (_hasChangedIcon) {
if (_hasCustomIcon) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
} else {
entry.setIcon(null);
}
}
return entry;
}
private void onSaveError(String msg) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(getString(R.string.saving_profile_error))
.setMessage(msg)
.setPositiveButton(android.R.string.ok, null)
.create());
}
private boolean onSave() {
DatabaseEntry entry;
try {
entry = parseEntry();
} catch (ParseException e) {
onSaveError(e.getMessage());
return false;
}
finish(entry, false);
return true;
}
private TextWatcher _iconChangeListener = new TextWatcher() {
@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 (!_hasCustomIcon) {
TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString(), _iconView);
_iconView.setImageDrawable(drawable);
}
}
};
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equals(string)) {
return i;
}
}
return -1;
}
private static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
final int width = !drawable.getBounds().isEmpty() ? drawable
.getBounds().width() : drawable.getIntrinsicWidth();
final int height = !drawable.getBounds().isEmpty() ? drawable
.getBounds().height() : drawable.getIntrinsicHeight();
final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width,
height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private static DatabaseEntry cloneEntry(DatabaseEntry entry) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(entry);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (DatabaseEntry) ois.readObject();
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,81 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import java.text.Collator;
import java.util.ArrayList;
import java.util.TreeSet;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private TreeSet<String> _groups;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_groups);
Intent intent = getIntent();
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
// set up the recycler view
_adapter = new GroupAdapter(this);
RecyclerView slotsView = findViewById(R.id.list_slots);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
slotsView.setLayoutManager(layoutManager);
slotsView.setAdapter(_adapter);
slotsView.setNestedScrollingEnabled(false);
for (String group : _groups) {
_adapter.addGroup(group);
}
}
@Override
public void onRemoveGroup(String group) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.remove_group)
.setMessage(R.string.remove_group_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_groups.remove(group);
_adapter.removeGroup(group);
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("groups", new ArrayList<>(_groups));
setResult(RESULT_OK, intent);
finish();
}
}

View file

@ -0,0 +1,204 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.WindowManager;
import com.github.paolorotolo.appintro.AppIntro2;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.slots.SlotException;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
private CustomAuthenticatedSlide _authenticatedSlide;
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private Database _database;
private DatabaseFile _databaseFile;
private PasswordSlot _passwordSlot;
private Cipher _passwordCipher;
private Preferences _prefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
setWizardMode(true);
showSkipButton(false);
pager.setPagingEnabled(false);
//showPagerIndicator(false);
setGoBackLock(true);
setBarColor(getResources().getColor(R.color.colorPrimary));
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_authenticationSlide = new CustomAuthenticationSlide();
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
//_authenticationSlide.setDescColor(getResources().getColor(R.color.primary_text_dark));
addSlide(_authenticationSlide);
_authenticatedSlide = new CustomAuthenticatedSlide();
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticatedSlide);
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
// create the database and database file
_database = new Database();
_databaseFile = new DatabaseFile();
}
private void setException(Exception e) {
Intent result = new Intent();
result.putExtra("exception", e);
setResult(RESULT_EXCEPTION, result);
finish();
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
Intent intent = getIntent();
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_passwordSlot = new PasswordSlot();
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
new DerivationTask(this, this).execute(params);
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
getPager().setCurrentItem(4);
}
}
}
@Override
public void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
int cryptType = _authenticatedSlide.getCryptType();
// wait for the key derivation background task
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
(_passwordSlot == null || _passwordCipher == null)) {
return;
}
// generate the master key
DatabaseFileCredentials creds = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
creds = new DatabaseFileCredentials();
}
SlotList slots = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
if (_passwordSlot == null || _passwordCipher == null) {
throw new RuntimeException();
}
try {
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
creds.getSlots().add(_passwordSlot);
} catch (SlotException e) {
setException(e);
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
try {
// encrypt the master key with the fingerprint key
// and add it to the list of slots
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
} catch (SlotException e) {
setException(e);
return;
}
}
// finally, save the database
try {
JSONObject obj = _database.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_databaseFile.setContent(obj);
} else {
_databaseFile.setContent(obj, creds);
}
DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (DatabaseManagerException | DatabaseFileException e) {
setException(e);
return;
}
// send the master key back to the main activity
Intent result = new Intent();
result.putExtra("creds", creds);
setResult(RESULT_OK, result);
// skip the intro from now on
_prefs.setIntroDone(true);
finish();
}
@Override
public void onTaskFinished(SecretKey key) {
if (key != null) {
try {
_passwordCipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
setException(e);
}
} else {
setException(new NullPointerException());
}
}
}

View file

@ -0,0 +1,496 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.TreeSet;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseManager;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
private static final int CODE_SCAN = 0;
private static final int CODE_ADD_ENTRY = 1;
private static final int CODE_EDIT_ENTRY = 2;
private static final int CODE_ENTER_ENTRY = 3;
private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6;
// permission request codes
private static final int CODE_PERM_CAMERA = 0;
private AegisApplication _app;
private DatabaseManager _db;
private boolean _loaded;
private String _checkedGroup;
private Menu _menu;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
_db = _app.getDatabaseManager();
_loaded = false;
// set up the main view
setContentView(R.layout.activity_main);
// set up the entry view
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
});
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// collapse the fab menu on touch
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (_fabMenu.isExpanded()) {
Rect rect = new Rect();
_fabMenu.getGlobalVisibleRect(rect);
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
_fabMenu.collapse();
}
}
}
return super.dispatchTouchEvent(event);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
if (!doShortcutActions() || _db.isLocked()) {
unlockDatabase(null);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data == null) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(resultCode, data);
break;
case CODE_ADD_ENTRY:
onAddEntryResult(resultCode, data);
break;
case CODE_EDIT_ENTRY:
onEditEntryResult(resultCode, data);
break;
case CODE_ENTER_ENTRY:
onEnterEntryResult(resultCode, data);
break;
case CODE_DO_INTRO:
onDoIntroResult(resultCode, data);
break;
case CODE_DECRYPT:
onDecryptResult(resultCode, data);
break;
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
break;
}
}
@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;
}
switch (requestCode) {
case CODE_PERM_CAMERA:
startScanActivity();
break;
}
}
private void onPreferencesResult(int resultCode, Intent data) {
// refresh the entire entry list if needed
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
_entryListView.setShowAccountName(showAccountName);
_entryListView.refresh(true);
}
}
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (entry != null) {
intent.putExtra("entry", entry);
}
intent.putExtra("isNew", isNew);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
startActivityForResult(intent, requestCode);
}
private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
}
}
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
if (data.getBooleanExtra("delete", false)) {
deleteEntry(entry);
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
_db.replaceEntry(entry);
_entryListView.replaceEntry(entry);
saveDatabase();
}
}
}
private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
}
}
private void updateGroupFilterMenu() {
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
for (int i = menu.size() - 1; i >= 0; i--) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.menu_filter_all) {
continue;
}
menu.removeItem(item.getItemId());
}
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _db.getGroups();
if (_checkedGroup != null && !groups.contains(_checkedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
}
for (String group : groups) {
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
if (group.equals(_checkedGroup)) {
item.setChecked(true);
}
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_checkedGroup = group;
_entryListView.setGroupFilter(group);
}
private void addEntry(DatabaseEntry entry) {
_db.addEntry(entry);
_entryListView.addEntry(entry);
}
private void onDoIntroResult(int resultCode, Intent data) {
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
// TODO: user feedback
Exception e = (Exception) data.getSerializableExtra("exception");
throw new UndeclaredThrowableException(e);
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
unlockDatabase(creds);
}
private void onDecryptResult(int resultCode, Intent intent) {
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
unlockDatabase(creds);
doShortcutActions();
}
private void startScanActivity() {
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
return;
}
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
startActivityForResult(scannerActivity, CODE_SCAN);
}
private boolean doShortcutActions() {
// return false if an action was blocked by a locked database
// otherwise, always return true
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null) {
return true;
} else if (_db.isLocked()) {
return false;
}
switch (action) {
case "scan":
startScanActivity();
break;
}
intent.removeExtra("action");
return true;
}
public void startActivityForResult(Intent intent, int requestCode) {
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
super.startActivityForResult(intent, requestCode);
}
@Override
protected void onResume() {
super.onResume();
if (_db.isLocked()) {
// start the intro if the database file doesn't exist
if (!_db.isLoaded() && !_db.fileExists()) {
// the db doesn't exist, start the intro
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
Intent intro = new Intent(this, IntroActivity.class);
startActivityForResult(intro, CODE_DO_INTRO);
return;
} else {
unlockDatabase(null);
}
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
updateGroupFilterMenu();
}
// refresh all codes to prevent showing old ones
_entryListView.refresh(true);
} else {
loadEntries();
}
updateLockIcon();
}
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
dialog.setCancelable(true);
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
dialog.show();
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
dialog.dismiss();
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
});
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteEntry(entry);
// update the filter list if the group no longer exists
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
});
});
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
dialog.dismiss();
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
});
return dialog;
}
private void deleteEntry(DatabaseEntry entry) {
_db.removeEntry(entry);
saveDatabase();
_entryListView.removeEntry(entry);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
return true;
case R.id.action_lock:
lockDatabase();
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
item.setChecked(true);
String group = null;
if (item.getItemId() != R.id.menu_filter_all) {
group = item.getTitle().toString();
}
setGroupFilter(group);
}
return super.onOptionsItemSelected(item);
}
}
private void lockDatabase() {
if (_loaded) {
_entryListView.clearEntries();
_db.lock();
_loaded = false;
startAuthActivity();
}
}
private void unlockDatabase(DatabaseFileCredentials creds) {
if (_loaded) {
return;
}
try {
if (!_db.isLoaded()) {
_db.load();
}
if (_db.isLocked()) {
if (creds == null) {
startAuthActivity();
return;
} else {
_db.unlock(creds);
}
}
} catch (DatabaseManagerException e) {
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
startAuthActivity();
return;
}
loadEntries();
}
private void loadEntries() {
// load all entries
_entryListView.addEntries(_db.getEntries());
_loaded = true;
}
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("slots", _db.getFileHeader().getSlots());
startActivityForResult(intent, CODE_DECRYPT);
}
private void saveDatabase() {
try {
_db.save();
} catch (DatabaseManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
}
}
private void updateLockIcon() {
// hide the lock icon if the database is not unlocked
if (_menu != null && !_db.isLocked()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_db.isEncryptionEnabled());
}
}
@Override
public void onEntryClick(DatabaseEntry entry) {
createBottomSheet(entry).show();
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_db.swapEntries(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
saveDatabase();
}
@Override
public void onEntryChange(DatabaseEntry entry) {
saveDatabase();
}
}

View file

@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_fragment = new PreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
// pass permission request results to the fragment
_fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onRestoreInstanceState(final Bundle inState) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
_fragment.setResult(inState.getParcelable("result"));
}
super.onRestoreInstanceState(inState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
// save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", _fragment.getResult());
super.onSaveInstanceState(outState);
}
}

View file

@ -0,0 +1,555 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
private static final int CODE_IMPORT_DECRYPT = 1;
private static final int CODE_SLOTS = 2;
private static final int CODE_GROUPS = 3;
// permission request codes
private static final int CODE_PERM_IMPORT = 0;
private static final int CODE_PERM_EXPORT = 1;
private Intent _result;
private DatabaseManager _db;
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseImporter _importer;
private Class<? extends DatabaseImporter> _importerType;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
@Override
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_db = app.getDatabaseManager();
// set the result intent in advance
setResult(new Intent());
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
}
});
Preference exportPreference = findPreference("pref_import");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
return true;
}
});
Preference importPreference = findPreference("pref_export");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onExport();
return true;
}
});
/*EditTextPreference timeoutPreference = (EditTextPreference) findPreference("pref_timeout");
timeoutPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
preference.setSummary(String.format(getString(R.string.pref_timeout_summary), (String) newValue));
return true;
}
});
timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());*/
Preference issuerPreference = findPreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRefresh", true);
return true;
}
});
Preference screenPreference = findPreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean)newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
return true;
}
});
_encryptionPreference = (SwitchPreference) findPreference("pref_encryption");
_encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (!_db.isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.disable_encryption))
.setMessage(getString(R.string.disable_encryption_description))
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
_db.disableEncryption();
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.encrypting_error), Toast.LENGTH_SHORT).show();
}
updateEncryptionPreferences();
}
})
.setNegativeButton(android.R.string.no, null)
.create());
}
return false;
}
});
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(FingerprintSlot.class)) {
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
} else {
// remove the fingerprint slot
FingerprintSlot slot = slots.find(FingerprintSlot.class);
slots.remove(slot);
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
return false;
}
});
_setPasswordPreference = findPreference("pref_password");
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
return false;
});
_slotsPreference = findPreference("pref_slots");
_slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", _db.getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
}
});
_groupsPreference = findPreference("pref_groups");
_groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
}
});
updateEncryptionPreferences();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!PermissionHelper.checkResults(grantResults)) {
Toast.makeText(getActivity(), getString(R.string.permission_denied), Toast.LENGTH_SHORT).show();
return;
}
switch (requestCode) {
case CODE_PERM_IMPORT:
onImport();
break;
case CODE_PERM_EXPORT:
onExport();
break;
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data == null) {
return;
}
switch (requestCode) {
case CODE_IMPORT:
onImportResult(resultCode, data);
break;
case CODE_IMPORT_DECRYPT:
onImportDecryptResult(resultCode, data);
break;
case CODE_SLOTS:
onSlotManagerResult(resultCode, data);
break;
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
}
}
public Intent getResult() {
return _result;
}
public void setResult(Intent result) {
_result = result;
getActivity().setResult(Activity.RESULT_OK, _result);
}
private void onImport() {
if (!PermissionHelper.request(getActivity(), CODE_PERM_IMPORT, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return;
}
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_importerType = importers.get(names[i]);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
}
})
.create());
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importer = null;
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
((AegisImporter)_importer).setCredentials(creds);
try {
importDatabase(_importer);
} catch (DatabaseImporterException e) {
Toast.makeText(getActivity(), getString(R.string.parsing_file_error), Toast.LENGTH_SHORT).show();
}
_importer = null;
}
private void onImportResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
ByteInputStream stream;
try (InputStream fileStream = getActivity().getContentResolver().openInputStream(uri)) {
stream = ByteInputStream.create(fileStream);
} catch (FileNotFoundException e) {
Toast.makeText(getActivity(), getString(R.string.file_not_found), Toast.LENGTH_SHORT).show();
return;
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getActivity(), getString(R.string.reading_file_error), Toast.LENGTH_SHORT).show();
return;
}
try {
DatabaseImporter importer = DatabaseImporter.create(stream, _importerType);
importer.parse();
// special case to decrypt encrypted aegis databases
if (importer.isEncrypted() && importer instanceof AegisImporter) {
_importer = importer;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter)_importer).getFile().getHeader().getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
Toast.makeText(getActivity(), getString(R.string.parsing_file_error), Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
List<DatabaseEntry> entries = importer.convert();
for (DatabaseEntry entry : entries) {
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
entry.resetUUID();
}
_db.addEntry(entry);
}
if (!saveDatabase()) {
return;
}
_result.putExtra("needsRecreate", true);
Toast.makeText(getActivity(), String.format(Locale.getDefault(), getString(R.string.imported_entries_count), entries.size()), Toast.LENGTH_LONG).show();
}
private void onExport() {
if (!PermissionHelper.request(getActivity(), CODE_PERM_EXPORT, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return;
}
// TODO: create a custom layout to show a message AND a checkbox
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle("Export the database")
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename;
try {
filename = _db.export(checked.get());
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.exporting_database_error), Toast.LENGTH_SHORT).show();
return;
}
// make sure the new file is visible
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
Toast.makeText(getActivity(), getString(R.string.export_database_location) + filename, Toast.LENGTH_SHORT).show();
})
.setNegativeButton(android.R.string.cancel, null);
if (_db.isEncryptionEnabled()) {
final String[] items = {"Keep the database encrypted"};
final boolean[] checkedItems = {true};
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int index, boolean isChecked) {
checked.set(isChecked);
}
});
} else {
builder.setMessage(getString(R.string.export_warning));
}
Dialogs.showSecureDialog(builder.create());
}
private void onSlotManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
private void onGroupManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (DatabaseEntry entry : _db.getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
}
private boolean saveDatabase() {
try {
_db.save();
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.saving_error), Toast.LENGTH_LONG).show();
return false;
}
return true;
}
private void updateEncryptionPreferences() {
boolean encrypted = _db.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_fingerprintPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
if (encrypted) {
SlotList slots = _db.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
_setPasswordPreference.setEnabled(!multiPassword);
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
_slotsPreference.setVisible(showSlots);
} else {
_setPasswordPreference.setEnabled(false);
_fingerprintPreference.setEnabled(false);
_fingerprintPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
}
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
try {
// encrypt the master key for this slot
slot.setKey(creds.getKey(), cipher);
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
slots.remove(oldSlot);
// add the new master password slot
slots.add(slot);
} catch (SlotException e) {
onException(e);
return;
}
_db.setCredentials(creds);
saveDatabase();
}
@Override
public void onException(Exception e) {
updateEncryptionPreferences();
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private class RegisterFingerprintListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
slots.add(slot);
_db.setCredentials(creds);
saveDatabase();
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = new DatabaseFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_db.enableEncryption(creds);
} catch (DatabaseManagerException | SlotException e) {
onException(e);
return;
}
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -0,0 +1,161 @@
package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import java.util.Collections;
import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView _scannerView;
private Menu _menu;
private int _facing = CAMERA_FACING_BACK;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
_scannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
return new SquareFinderView(context);
}
};
_scannerView.setResultHandler(this);
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
int camera = getRearCameraId();
if (camera == -1) {
camera = getFrontCameraId();
if (camera == -1) {
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
finish();
}
_facing = CAMERA_FACING_FRONT;
}
_scannerView.startCamera(camera);
setContentView(_scannerView);
}
@Override
protected void setPreferredTheme(boolean darkMode) {
setTheme(R.style.AppTheme_Fullscreen);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_scanner, menu);
updateCameraIcon();
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_camera:
_scannerView.stopCamera();
switch (_facing) {
case CAMERA_FACING_BACK:
_facing = CAMERA_FACING_FRONT;
break;
case CAMERA_FACING_FRONT:
_facing = CAMERA_FACING_BACK;
break;
}
updateCameraIcon();
_scannerView.startCamera(getCameraId(_facing));
return true;
case R.id.action_lock:
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onResume() {
super.onResume();
_scannerView.startCamera(getCameraId(_facing));
}
@Override
public void onPause() {
super.onPause();
_scannerView.stopCamera();
}
@Override
public void handleResult(Result rawResult) {
try {
// parse google auth uri
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
DatabaseEntry entry = new DatabaseEntry(info);
Intent intent = new Intent();
intent.putExtra("entry", entry);
setResult(RESULT_OK, intent);
finish();
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
}
_scannerView.resumeCameraPreview(this);
}
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
if (dual) {
switch (_facing) {
case CAMERA_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CAMERA_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
}
item.setVisible(dual);
}
}
private static int getCameraId(int facing) {
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
Camera.getCameraInfo(i, info);
if (info.facing == facing) {
return i;
}
}
return -1;
}
private static int getRearCameraId() {
return getCameraId(CAMERA_FACING_BACK);
}
private static int getFrontCameraId() {
return getCameraId(CAMERA_FACING_FRONT);
}
}

View file

@ -0,0 +1,191 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
import javax.crypto.Cipher;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
private SlotAdapter _adapter;
private boolean _edited;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_slots);
_edited = false;
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
Dialogs.showFingerprintDialog(this ,this);
});
findViewById(R.id.button_add_password).setOnClickListener(view -> {
Dialogs.showSetPasswordDialog(this, this);
});
// set up the recycler view
_adapter = new SlotAdapter(this);
RecyclerView slotsView = findViewById(R.id.list_slots);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
slotsView.setLayoutManager(layoutManager);
slotsView.setAdapter(_adapter);
slotsView.setNestedScrollingEnabled(false);
// load the slots and masterKey
_creds = (DatabaseFileCredentials) getIntent().getSerializableExtra("creds");
for (Slot slot : _creds.getSlots()) {
_adapter.addSlot(slot);
}
updateFingerprintButton();
}
private void updateFingerprintButton() {
// only show the fingerprint option if we can get an instance of the fingerprint manager
// and if none of the slots in the collection has a matching alias in the keystore
int visibility = View.VISIBLE;
if (FingerprintHelper.getManager(this) != null) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
if (keyStore.containsKey(slot.getUUID().toString())) {
visibility = View.GONE;
break;
}
}
} catch (KeyStoreHandleException e) {
visibility = View.GONE;
}
} else {
visibility = View.GONE;
}
findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
}
private void onSave() {
Intent intent = new Intent();
intent.putExtra("creds", _creds);
setResult(RESULT_OK, intent);
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.action_save:
onSave();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_slots, menu);
return true;
}
@Override
public void onBackPressed() {
if (!_edited) {
super.onBackPressed();
return;
}
Dialogs.showDiscardDialog(this,
(dialog, which) -> onSave(),
(dialog, which) -> super.onBackPressed()
);
}
@Override
public void onEditSlot(Slot slot) {
/*EditText textName = new EditText(this);
textName.setHint("Name");
new AlertDialog.Builder(this)
.setTitle("Edit slot name")
.setView(textName)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
String name = textName.getText().toString();
_edited = true;
})
.setNegativeButton(android.R.string.cancel, null)
.show();*/
}
@Override
public void onRemoveSlot(Slot slot) {
SlotList slots = _creds.getSlots();
if (slot instanceof PasswordSlot && slots.findAll(PasswordSlot.class).size() <= 1) {
Toast.makeText(this, R.string.password_slot_error, Toast.LENGTH_SHORT).show();
return;
}
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.remove_slot)
.setMessage(R.string.remove_slot_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
slots.remove(slot);
_adapter.removeSlot(slot);
_edited = true;
updateFingerprintButton();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
_creds.getSlots().add(slot);
_adapter.addSlot(slot);
_edited = true;
updateFingerprintButton();
}
@Override
public void onException(Exception e) {
Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}

View file

@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.ui.preferences;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import android.util.AttributeSet;
public class SwitchPreference extends SwitchPreferenceCompat {
private Preference.OnPreferenceChangeListener _listener;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwitchPreference(Context context) {
super(context);
}
@Override
public void setOnPreferenceChangeListener(OnPreferenceChangeListener listener) {
super.setOnPreferenceChangeListener(listener);
_listener = listener;
}
@Override
public void setChecked(boolean checked) {
setChecked(true, false);
}
public void setChecked(boolean checked, boolean silent) {
if (silent) {
super.setOnPreferenceChangeListener(null);
}
super.setChecked(checked);
if (silent) {
super.setOnPreferenceChangeListener(_listener);
}
}
}

View file

@ -0,0 +1,181 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
private EditText _textPassword;
private EditText _textPasswordConfirm;
private int _bgColor;
private LinearLayout _boxFingerprint;
private SwirlView _imgFingerprint;
private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher;
private boolean _fingerAuthenticated;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_boxFingerprint = view.findViewById(R.id.box_fingerprint);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert);
_imgFingerprint = new SwirlView(getContext());
insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
_textFingerprint = view.findViewById(R.id.text_fingerprint);
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
public int getCryptType() {
return _cryptType;
}
public char[] getPassword() {
return EditTextHelper.getEditTextChars(_textPassword);
}
public Cipher getFingerCipher() {
return _fingerCipher;
}
public FingerprintSlot getFingerSlot() {
return _fingerSlot;
}
public void setBgColor(int color) {
_bgColor = color;
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
switch(_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
break;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
_boxFingerprint.setVisibility(View.VISIBLE);
SecretKey key;
try {
if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle();
_fingerSlot = new FingerprintSlot();
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new UndeclaredThrowableException(e);
}
if (_fingerHelper == null) {
FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
_fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this);
}
try {
_fingerCipher = Slot.createEncryptCipher(key);
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
break;
default:
throw new RuntimeException();
}
}
@Override
public void onSlideDeselected() {
if (_fingerHelper != null) {
_fingerAuthenticated = false;
_boxFingerprint.setVisibility(View.INVISIBLE);
_fingerHelper.stopListening();
}
}
@Override
public boolean isPolicyRespected() {
switch(_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
if (!_fingerAuthenticated) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
default:
throw new RuntimeException();
}
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
message = getString(R.string.password_equality_error);
} else if (!_fingerAuthenticated) {
message = getString(R.string.register_fingerprint);
} else {
return;
}
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
}
@Override
public void onAuthenticated() {
_fingerAuthenticated = true;
}
@Override
public void onError() {
}
}

View file

@ -0,0 +1,88 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.R;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_FINGER = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authentication_slide, container, false);
_buttonGroup = view.findViewById(R.id.rg_authenticationMethod);
_buttonGroup.setOnCheckedChangeListener(this);
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
FingerprintManager manager = FingerprintHelper.getManager(getContext());
if (manager != null) {
RadioButton button = view.findViewById(R.id.rb_fingerprint);
TextView text = view.findViewById(R.id.text_rb_fingerprint);
button.setEnabled(true);
text.setEnabled(true);
}
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
public void setBgColor(int color) {
_bgColor = color;
}
@Override
public boolean isPolicyRespected() {
return _buttonGroup.getCheckedRadioButtonId() != -1;
}
@Override
public void onUserIllegallyRequestedNextPage() {
Snackbar snackbar = Snackbar.make(getView(), getString(R.string.snackbar_authentication_method), Snackbar.LENGTH_LONG);
snackbar.show();
}
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if (i == -1) {
return;
}
int id;
switch (i) {
case R.id.rb_none:
id = CRYPT_TYPE_NONE;
break;
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
break;
case R.id.rb_fingerprint:
id = CRYPT_TYPE_FINGER;
break;
default:
throw new RuntimeException();
}
Intent intent = getActivity().getIntent();
intent.putExtra("cryptType", id);
}
}

View file

@ -0,0 +1,64 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
private Callback _cb;
public DerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.encrypting_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(DerivationTask.Params... args) {
setPriority();
Params params = args[0];
byte[] salt = CryptoUtils.generateSalt();
SCryptParameters scryptParams = new SCryptParameters(
CryptoUtils.CRYPTO_SCRYPT_N,
CryptoUtils.CRYPTO_SCRYPT_r,
CryptoUtils.CRYPTO_SCRYPT_p,
salt
);
return params.getSlot().deriveKey(params.getPassword(), scryptParams);
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public static class Params {
private PasswordSlot _slot;
private char[] _password;
public Params(PasswordSlot slot, char[] password) {
_slot = slot;
_password = password;
}
public PasswordSlot getSlot() {
return _slot;
}
public char[] getPassword() {
return _password;
}
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}

View file

@ -0,0 +1,41 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Process;
import androidx.annotation.CallSuper;
import com.beemdevelopment.aegis.ui.Dialogs;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
private ProgressDialog _dialog;
public ProgressDialogTask(Context context, String message) {
_dialog = new ProgressDialog(context);
_dialog.setCancelable(false);
_dialog.setMessage(message);
Dialogs.secureDialog(_dialog);
}
@CallSuper
@Override
protected void onPreExecute() {
_dialog.show();
}
@CallSuper
@Override
protected void onPostExecute(Result result) {
if (_dialog.isShowing()) {
_dialog.dismiss();
}
}
protected void setPriority() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
}
protected final ProgressDialog getDialog() {
return _dialog;
}
}

View file

@ -0,0 +1,98 @@
package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.crypto.MasterKey;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
private Callback _cb;
private Class<T> _type;
public SlotListTask(Class<T> type, Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
_type = type;
}
@Override
protected MasterKey doInBackground(SlotListTask.Params... args) {
setPriority();
Params params = args[0];
SlotList slots = params.getSlots();
try {
if (!slots.has(_type)) {
throw new RuntimeException();
}
MasterKey masterKey = null;
for (Slot slot : slots.findAll(_type)) {
try {
if (slot instanceof PasswordSlot) {
char[] password = (char[])params.getObj();
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
Cipher cipher = slot.createDecryptCipher(key);
masterKey = slot.getKey(cipher);
} else if (slot instanceof FingerprintSlot) {
masterKey = slot.getKey((Cipher)params.getObj());
} else {
throw new RuntimeException();
}
break;
} catch (SlotIntegrityException e) {
}
}
if (masterKey == null) {
throw new SlotIntegrityException();
}
return masterKey;
} catch (SlotIntegrityException e) {
return null;
} catch (SlotException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onPostExecute(MasterKey masterKey) {
super.onPostExecute(masterKey);
_cb.onTaskFinished(masterKey);
}
public static class Params {
private SlotList _slots;
private Object _obj;
public Params(SlotList slots, Object obj) {
_slots = slots;
_obj = obj;
}
public SlotList getSlots() {
return _slots;
}
public Object getObj() {
return _obj;
}
}
public interface Callback {
void onTaskFinished(MasterKey key);
}
}

View file

@ -0,0 +1,271 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private boolean _showAccountName;
private String _groupFilter;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(Listener listener) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_holders = new ArrayList<>();
_listener = listener;
}
public void setShowAccountName(boolean showAccountName) {
_showAccountName = showAccountName;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void addEntries(List<DatabaseEntry> entries) {
_entries.addAll(entries);
for (DatabaseEntry entry : entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
notifyDataSetChanged();
}
public void removeEntry(DatabaseEntry entry) {
entry = getEntryByUUID(entry.getUUID());
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
int position = _shownEntries.indexOf(entry);
_shownEntries.remove(position);
notifyItemRemoved(position);
}
}
public void clearEntries() {
_entries.clear();
_shownEntries.clear();
notifyDataSetChanged();
}
public void replaceEntry(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
int position = _shownEntries.indexOf(oldEntry);
if (isEntryFiltered(newEntry)) {
_shownEntries.remove(position);
notifyItemRemoved(position);
} else {
_shownEntries.set(position, newEntry);
notifyItemChanged(position);
}
} else if (!isEntryFiltered(newEntry)) {
// TODO: preserve order
_shownEntries.add(newEntry);
int position = getItemCount() - 1;
notifyItemInserted(position);
}
}
private boolean isEntryFiltered(DatabaseEntry entry) {
String group = entry.getGroup();
if (_groupFilter == null) {
return false;
}
return group == null || !group.equals(_groupFilter);
}
private DatabaseEntry getEntryByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
throw new AssertionError("no entry found with the same id");
}
public void refresh(boolean hard) {
if (hard) {
notifyDataSetChanged();
} else {
for (EntryHolder holder : _holders) {
holder.refreshCode();
}
}
}
public void setGroupFilter(String group) {
_groupFilter = group;
_shownEntries.clear();
for (DatabaseEntry entry : _entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
notifyDataSetChanged();
}
@Override
public void onItemDismiss(int position) {
}
@Override
public void onItemDrop(int position) {
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
return;
}
_listener.onEntryDrop(_shownEntries.get(position));
}
@Override
public void onItemMove(int firstPosition, int secondPosition) {
// moving entries is not allowed when a filter is applied
if (_groupFilter != null) {
return;
}
// notify the database first
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
Collections.swap(_shownEntries, firstPosition, secondPosition);
notifyItemMoved(firstPosition, secondPosition);
}
@Override
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
return new EntryHolder(view);
}
@Override
public void onViewRecycled(EntryHolder holder) {
holder.stopRefreshLoop();
_holders.remove(holder);
}
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _shownEntries.get(position);
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress);
if (showProgress) {
holder.startRefreshLoop();
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
_listener.onEntryClick(_shownEntries.get(position));
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
return _listener.onLongEntryClick(_shownEntries.get(position));
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// this will only be called if the entry is of type HotpInfo
try {
((HotpInfo)entry.getInfo()).incrementCounter();
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
// notify the listener that the counter has been incremented
// this gives it a chance to save the database
_listener.onEntryChange(entry);
// finally, refresh the code in the UI
holder.refreshCode();
}
});
_holders.add(holder);
}
public int getUniformPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (DatabaseEntry entry : _shownEntries) {
OtpInfo info = entry.getInfo();
if (info instanceof TotpInfo) {
infos.add((TotpInfo) info);
}
}
if (infos.isEmpty()) {
return -1;
}
int period = infos.get(0).getPeriod();
for (TotpInfo info : infos) {
if (period != info.getPeriod()) {
return -1;
}
}
return period;
}
public boolean isPeriodUniform() {
return getUniformPeriod() != -1;
}
@Override
public int getItemCount() {
return _shownEntries.size();
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
boolean onLongEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
}
}

View file

@ -0,0 +1,105 @@
package com.beemdevelopment.aegis.ui.views;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EntryHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private PeriodProgressBar _progressBar;
private UiRefresher _refresher;
public EntryHolder(final View view) {
super(view);
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refreshCode();
_progressBar.refresh();
}
@Override
public long getMillisTillNextRefresh() {
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
}
});
}
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress) {
_entry = entry;
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
if (showProgress) {
_progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
}
// only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
_profileIssuer.setText(entry.getIssuer());
_profileName.setText("");
if (showAccountName) {
_profileName.setText(" - " + entry.getName());
}
if (_entry.hasIcon()) {
byte[] imageBytes = entry.getIcon();
Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_profileDrawable.setImageBitmap(image);
} else {
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
refreshCode();
}
public void setOnRefreshClickListener(View.OnClickListener listener) {
_buttonRefresh.setOnClickListener(listener);
}
public void startRefreshLoop() {
_refresher.start();
}
public void stopRefreshLoop() {
_refresher.stop();
}
public void refreshCode() {
String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
}
}

View file

@ -0,0 +1,195 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import java.util.List;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private RecyclerView _rvKeyProfiles;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
private UiRefresher _refresher;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_adapter = new EntryAdapter(this);
_showProgress = false;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
// set up the recycler view
_rvKeyProfiles = view.findViewById(R.id.rvKeyProfiles);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_rvKeyProfiles.setLayoutManager(mLayoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(_rvKeyProfiles);
_rvKeyProfiles.setAdapter(_adapter);
int resId = R.anim.layout_animation_fall_down;
LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(getContext(), resId);
_rvKeyProfiles.setLayoutAnimation(animation);
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refresh(false);
}
@Override
public long getMillisTillNextRefresh() {
return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
}
});
return view;
}
public void setGroupFilter(String group) {
_adapter.setGroupFilter(group);
_touchCallback.setIsLongPressDragEnabled(group == null);
checkPeriodUniformity();
runLayoutAnimation(_rvKeyProfiles);
}
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.refresh();
}
_adapter.refresh(hard);
}
private void checkPeriodUniformity() {
boolean uniform = _adapter.isPeriodUniform();
if (uniform == _showProgress) {
return;
}
_showProgress = uniform;
if (_showProgress) {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
startRefreshLoop();
} else {
_progressBar.setVisibility(View.GONE);
stopRefreshLoop();
}
}
private void startRefreshLoop() {
refresh(true);
_refresher.start();
}
private void stopRefreshLoop() {
refresh(true);
_refresher.stop();
}
public void setListener(Listener listener) {
_listener = listener;
}
@Override
public void onEntryClick(DatabaseEntry entry) {
_listener.onEntryClick(entry);
}
@Override
public boolean onLongEntryClick(DatabaseEntry entry) {
return false;
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_listener.onEntryMove(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
_listener.onEntryDrop(entry);
}
@Override
public void onEntryChange(DatabaseEntry entry) {
_listener.onEntryChange(entry);
}
public void setShowAccountName(boolean showAccountName) {
_adapter.setShowAccountName(showAccountName);
}
public void addEntry(DatabaseEntry entry) {
_adapter.addEntry(entry);
checkPeriodUniformity();
}
public void addEntries(List<DatabaseEntry> entries) {
_adapter.addEntries(entries);
checkPeriodUniformity();
}
public void removeEntry(DatabaseEntry entry) {
_adapter.removeEntry(entry);
checkPeriodUniformity();
}
public void clearEntries() {
_adapter.clearEntries();
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry entry) {
_adapter.replaceEntry(entry);
checkPeriodUniformity();
}
private void runLayoutAnimation(final RecyclerView recyclerView) {
final Context context = recyclerView.getContext();
final LayoutAnimationController controller =
AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down);
recyclerView.setLayoutAnimation(controller);
recyclerView.getAdapter().notifyDataSetChanged();
recyclerView.scheduleLayoutAnimation();
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
}
}

View file

@ -0,0 +1,61 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
private GroupAdapter.Listener _listener;
private ArrayList<String> _groups;
public GroupAdapter(GroupAdapter.Listener listener) {
_listener = listener;
_groups = new ArrayList<>();
}
public void addGroup(String group) {
_groups.add(group);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void removeGroup(String group) {
int position = _groups.indexOf(group);
_groups.remove(position);
notifyItemRemoved(position);
}
@Override
public GroupHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
return new GroupHolder(view);
}
@Override
public void onBindViewHolder(GroupHolder holder, int position) {
holder.setData(_groups.get(position));
holder.setOnDeleteClickListener(v -> {
int position12 = holder.getAdapterPosition();
_listener.onRemoveGroup(_groups.get(position12));
});
}
@Override
public int getItemCount() {
return _groups.size();
}
public interface Listener {
void onRemoveGroup(String group);
}
}

View file

@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
public class GroupHolder extends RecyclerView.ViewHolder {
private TextView _slotName;
private ImageView _buttonDelete;
public GroupHolder(final View view) {
super(view);
_slotName = view.findViewById(R.id.text_slot_name);
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(String groupName) {
_slotName.setText(groupName);
}
public void setOnDeleteClickListener(View.OnClickListener listener) {
_buttonDelete.setOnClickListener(listener);
}
}

View file

@ -0,0 +1,53 @@
package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import com.beemdevelopment.aegis.otp.TotpInfo;
public class PeriodProgressBar extends ProgressBar {
private int _period;
public PeriodProgressBar(Context context) {
super(context);
}
public PeriodProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public PeriodProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void setPeriod(int period) {
_period = period;
}
public void refresh() {
// reset the progress bar
int maxProgress = getMax();
setProgress(maxProgress);
// calculate the progress the bar should start at
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
long period = _period * maxProgress;
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
// start progress animation
ObjectAnimator animation = ObjectAnimator.ofInt(this, "progress", currentProgress, 0);
animation.setDuration(millisTillRotation);
animation.setInterpolator(new LinearInterpolator());
animation.start();
}
}

View file

@ -0,0 +1,71 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.Slot;
public class SlotAdapter extends RecyclerView.Adapter<SlotHolder> {
private Listener _listener;
private ArrayList<Slot> _slots;
public SlotAdapter(Listener listener) {
_listener = listener;
_slots = new ArrayList<>();
}
public void addSlot(Slot slot) {
_slots.add(slot);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
public void updateSlot(Slot slot) {
notifyItemChanged(_slots.indexOf(slot));
}
public void removeSlot(Slot slot) {
int position = _slots.indexOf(slot);
_slots.remove(position);
notifyItemRemoved(position);
}
@Override
public SlotHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_slot, parent, false);
return new SlotHolder(view);
}
@Override
public void onBindViewHolder(SlotHolder holder, int position) {
holder.setData(_slots.get(position));
holder.setOnEditClickListener(v -> {
int position1 = holder.getAdapterPosition();
_listener.onEditSlot(_slots.get(position1));
});
holder.setOnDeleteClickListener(v -> {
int position12 = holder.getAdapterPosition();
_listener.onRemoveSlot(_slots.get(position12));
});
}
@Override
public int getItemCount() {
return _slots.size();
}
public interface Listener {
void onEditSlot(Slot slot);
void onRemoveSlot(Slot slot);
}
}

View file

@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.RawSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
public class SlotHolder extends RecyclerView.ViewHolder {
private TextView _slotUsed;
private TextView _slotName;
private ImageView _slotImg;
private LinearLayout _buttonEdit;
private ImageView _buttonDelete;
public SlotHolder(final View view) {
super(view);
_slotUsed = view.findViewById(R.id.text_slot_used);
_slotName = view.findViewById(R.id.text_slot_name);
_slotImg = view.findViewById(R.id.img_slot);
_buttonEdit = view.findViewById(R.id.button_edit);
_buttonDelete = view.findViewById(R.id.button_delete);
}
public void setData(Slot slot) {
if (slot instanceof PasswordSlot) {
_slotName.setText(R.string.password);
_slotImg.setImageResource(R.drawable.ic_create_black_24dp);
} else if (slot instanceof FingerprintSlot) {
_slotName.setText(R.string.authentication_method_fingerprint);
_slotImg.setImageResource(R.drawable.ic_fingerprint_black_24dp);
if (FingerprintHelper.isSupported()) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(slot.getUUID().toString())) {
_slotUsed.setVisibility(View.VISIBLE);
}
} catch (KeyStoreHandleException e) { }
}
} else if (slot instanceof RawSlot) {
_slotName.setText(R.string.authentication_method_raw);
_slotImg.setImageResource(R.drawable.ic_vpn_key_black_24dp);
} else {
throw new RuntimeException();
}
}
public void setOnEditClickListener(View.OnClickListener listener) {
_buttonEdit.setOnClickListener(listener);
}
public void setOnDeleteClickListener(View.OnClickListener listener) {
_buttonDelete.setOnClickListener(listener);
}
}

View file

@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteInputStream extends ByteArrayInputStream {
private ByteInputStream(byte[] buf) {
super(buf);
}
public static ByteInputStream create(InputStream fileStream) throws IOException {
int read;
byte[] buf = new byte[4096];
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
outStream.write(buf, 0, read);
}
return new ByteInputStream(outStream.toByteArray());
}
public byte[] getBytes() {
return this.buf;
}
}