mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-25 11:22:25 +00:00
Merge branch 'master' of https://github.com/alexbakker/Aegis
This commit is contained in:
commit
9ec5525547
66 changed files with 1652 additions and 1167 deletions
|
@ -38,7 +38,7 @@ dependencies {
|
|||
implementation 'com.android.support:cardview-v7:27.1.1'
|
||||
implementation 'com.android.support:support-v4:27.1.1'
|
||||
implementation 'com.mattprecious.swirl:swirl:1.0.0'
|
||||
implementation 'com.madgag.spongycastle:core:1.56.0.0'
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
implementation 'com.github.apl-devs:appintro:v4.2.2'
|
||||
implementation 'com.getbase:floatingactionbutton:1.10.1'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
android:configChanges="keyboardHidden|orientation|screenSize">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.EditProfileActivity"
|
||||
android:name=".ui.EditEntryActivity"
|
||||
android:label="Edit profile"
|
||||
android:theme="@style/AppTheme.TransparentActionBar">
|
||||
</activity>
|
||||
|
|
|
@ -1,6 +1,46 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
public class CryptParameters {
|
||||
public byte[] Nonce;
|
||||
public byte[] Tag;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import me.impy.aegis.encoding.Hex;
|
||||
import me.impy.aegis.encoding.HexException;
|
||||
|
||||
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 parseJson(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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
public class CryptResult {
|
||||
public CryptParameters Parameters;
|
||||
public byte[] Data;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
|
@ -20,20 +20,17 @@ 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_HASH = "SHA-256";
|
||||
|
||||
public static final String CRYPTO_CIPHER_RAW = "AES/ECB/NoPadding";
|
||||
public static final byte CRYPTO_KEY_SIZE = 32;
|
||||
|
||||
public static final String CRYPTO_CIPHER_AEAD = "AES/GCM/NoPadding";
|
||||
public static final byte CRYPTO_TAG_SIZE = 16;
|
||||
public static final byte CRYPTO_NONCE_SIZE = 12;
|
||||
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
|
||||
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
|
||||
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
|
||||
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
|
||||
|
||||
public static final int CRYPTO_SCRYPT_N = 1 << 15;
|
||||
public static final int CRYPTO_SCRYPT_r = 8;
|
||||
|
@ -41,23 +38,42 @@ public class CryptoUtils {
|
|||
|
||||
public static SecretKey deriveKey(char[] password, byte[] salt, int n, int r, int p) {
|
||||
byte[] bytes = toBytes(password);
|
||||
byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_KEY_SIZE);
|
||||
byte[] keyBytes = SCrypt.generate(bytes, salt, n, r, p, CRYPTO_AEAD_KEY_SIZE);
|
||||
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
|
||||
}
|
||||
|
||||
public static Cipher createCipher(SecretKey key, int opmode)
|
||||
public static Cipher createEncryptCipher(SecretKey key)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException,
|
||||
InvalidAlgorithmParameterException, InvalidKeyException {
|
||||
byte[] nonce = generateNonce();
|
||||
return createCipher(key, opmode, nonce);
|
||||
return createCipher(key, Cipher.ENCRYPT_MODE, null);
|
||||
}
|
||||
|
||||
public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
|
||||
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 {
|
||||
IvParameterSpec spec = new IvParameterSpec(nonce);
|
||||
Cipher cipher = Cipher.getInstance(CRYPTO_CIPHER_AEAD);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -65,16 +81,10 @@ public class CryptoUtils {
|
|||
throws BadPaddingException, IllegalBlockSizeException {
|
||||
// split off the tag to store it separately
|
||||
byte[] result = cipher.doFinal(data);
|
||||
byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_TAG_SIZE, result.length);
|
||||
byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_TAG_SIZE);
|
||||
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() {{
|
||||
Parameters = new CryptParameters() {{
|
||||
Nonce = cipher.getIV();
|
||||
Tag = tag;
|
||||
}};
|
||||
Data = encrypted;
|
||||
}};
|
||||
return new CryptResult(encrypted, new CryptParameters(cipher.getIV(), tag));
|
||||
}
|
||||
|
||||
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
|
||||
|
@ -82,34 +92,18 @@ public class CryptoUtils {
|
|||
// append the tag to the ciphertext
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
stream.write(encrypted);
|
||||
stream.write(params.Tag);
|
||||
stream.write(params.getTag());
|
||||
|
||||
encrypted = stream.toByteArray();
|
||||
byte[] decrypted = cipher.doFinal(encrypted);
|
||||
|
||||
return new CryptResult() {{
|
||||
Parameters = params;
|
||||
Data = decrypted;
|
||||
}};
|
||||
}
|
||||
|
||||
public static byte[] hashKey(SecretKey key) {
|
||||
MessageDigest hash;
|
||||
try {
|
||||
hash = MessageDigest.getInstance(CRYPTO_HASH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
byte[] bytes = key.getEncoded();
|
||||
hash.update(bytes);
|
||||
return hash.digest();
|
||||
return new CryptResult(decrypted, params);
|
||||
}
|
||||
|
||||
public static SecretKey generateKey() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(CRYPTO_KEY_SIZE * 8);
|
||||
generator.init(CRYPTO_AEAD_KEY_SIZE * 8);
|
||||
return generator.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
|
@ -117,11 +111,7 @@ public class CryptoUtils {
|
|||
}
|
||||
|
||||
public static byte[] generateSalt() {
|
||||
return generateRandomBytes(CRYPTO_KEY_SIZE);
|
||||
}
|
||||
|
||||
public static byte[] generateNonce() {
|
||||
return generateRandomBytes(CRYPTO_NONCE_SIZE);
|
||||
return generateRandomBytes(CRYPTO_AEAD_KEY_SIZE);
|
||||
}
|
||||
|
||||
public static byte[] generateRandomBytes(int length) {
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
|
||||
public class KeyInfo implements Serializable {
|
||||
private String _type = "totp";
|
||||
private byte[] _secret;
|
||||
private String _accountName = "";
|
||||
private String _issuer = "";
|
||||
private long _counter = 0;
|
||||
private String _algorithm = "SHA1";
|
||||
private int _digits = 6;
|
||||
private int _period = 30;
|
||||
|
||||
public String getURL() {
|
||||
Uri.Builder builder = new Uri.Builder();
|
||||
builder.scheme("otpauth");
|
||||
builder.authority(_type);
|
||||
|
||||
builder.appendQueryParameter("digits", Integer.toString(_digits));
|
||||
builder.appendQueryParameter("period", Integer.toString(_period));
|
||||
builder.appendQueryParameter("algorithm", _algorithm);
|
||||
builder.appendQueryParameter("secret", new String(Base32.encode(_secret)));
|
||||
if (_type.equals("hotp")) {
|
||||
builder.appendQueryParameter("counter", Long.toString(_counter));
|
||||
}
|
||||
|
||||
if (_issuer != null && !_issuer.equals("")) {
|
||||
builder.path(String.format("%s:%s", _issuer, _accountName));
|
||||
builder.appendQueryParameter("issuer", _issuer);
|
||||
} else {
|
||||
builder.path(_accountName);
|
||||
}
|
||||
|
||||
return builder.build().toString();
|
||||
}
|
||||
|
||||
public long getMillisTillNextRotation() {
|
||||
return KeyInfo.getMillisTillNextRotation(_period);
|
||||
}
|
||||
|
||||
public static long getMillisTillNextRotation(int period) {
|
||||
long p = period * 1000;
|
||||
return p - (System.currentTimeMillis() % p);
|
||||
}
|
||||
|
||||
public static KeyInfo fromURL(String s) throws KeyInfoException {
|
||||
final Uri url = Uri.parse(s);
|
||||
if (!url.getScheme().equals("otpauth")) {
|
||||
throw new KeyInfoException("unsupported protocol");
|
||||
}
|
||||
|
||||
KeyInfo info = new KeyInfo();
|
||||
info.setType(url.getHost());
|
||||
|
||||
// 'secret' is a required parameter
|
||||
String secret = url.getQueryParameter("secret");
|
||||
if (secret == null) {
|
||||
throw new KeyInfoException("'secret' is not set");
|
||||
}
|
||||
info.setSecret(secret.toCharArray());
|
||||
|
||||
// provider info used to disambiguate accounts
|
||||
String path = url.getPath();
|
||||
String label = path != null && path.length() > 0 ? path.substring(1) : "";
|
||||
|
||||
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) {
|
||||
info.setIssuer(strings[0]);
|
||||
info.setAccountName(strings[1]);
|
||||
} else {
|
||||
// at this point, just dump the whole thing into the accountName
|
||||
info.setAccountName(label);
|
||||
}
|
||||
} else {
|
||||
// label only contains the account name
|
||||
// grab the issuer's info from the 'issuer' parameter if it's present
|
||||
String issuer = url.getQueryParameter("issuer");
|
||||
info.setIssuer(issuer != null ? issuer : "");
|
||||
info.setAccountName(label);
|
||||
}
|
||||
|
||||
// just use the defaults if these parameters aren't set
|
||||
String algorithm = url.getQueryParameter("algorithm");
|
||||
if (algorithm != null) {
|
||||
info.setAlgorithm(algorithm);
|
||||
}
|
||||
String period = url.getQueryParameter("period");
|
||||
if (period != null) {
|
||||
info.setPeriod(Integer.parseInt(period));
|
||||
}
|
||||
String digits = url.getQueryParameter("digits");
|
||||
if (digits != null) {
|
||||
info.setDigits(Integer.parseInt(digits));
|
||||
}
|
||||
|
||||
// 'counter' is required if the type is 'hotp'
|
||||
String counter = url.getQueryParameter("counter");
|
||||
if (counter != null) {
|
||||
info.setCounter(Long.parseLong(counter));
|
||||
} else if (info.getType().equals("hotp")) {
|
||||
throw new KeyInfoException("'counter' was not set which is required for 'hotp'");
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return _type;
|
||||
}
|
||||
|
||||
public byte[] getSecret() {
|
||||
return _secret;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return _accountName;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getAlgorithm(boolean java) {
|
||||
if (java) {
|
||||
return "Hmac" + _algorithm;
|
||||
}
|
||||
return _algorithm;
|
||||
}
|
||||
|
||||
public int getDigits() {
|
||||
return _digits;
|
||||
}
|
||||
|
||||
public long getCounter() {
|
||||
return _counter;
|
||||
}
|
||||
|
||||
public int getPeriod() {
|
||||
return _period;
|
||||
}
|
||||
|
||||
public boolean isTypeValid(String type) {
|
||||
return type.equals("totp") || type.equals("hotp");
|
||||
}
|
||||
|
||||
public void setType(String type) throws KeyInfoException {
|
||||
type = type.toLowerCase();
|
||||
if (!isTypeValid(type)) {
|
||||
throw new KeyInfoException(String.format("unsupported otp type: %s", type));
|
||||
}
|
||||
_type = type;
|
||||
}
|
||||
|
||||
public void setSecret(char[] base32) throws KeyInfoException {
|
||||
byte[] secret;
|
||||
try {
|
||||
secret = Base32.decode(base32);
|
||||
} catch (Base32Exception e) {
|
||||
throw new KeyInfoException("bad secret", e);
|
||||
}
|
||||
|
||||
setSecret(secret);
|
||||
}
|
||||
|
||||
public void setSecret(byte[] secret) {
|
||||
_secret = secret;
|
||||
}
|
||||
|
||||
public void setAccountName(String accountName) {
|
||||
_accountName = accountName;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
_issuer = issuer;
|
||||
}
|
||||
|
||||
public boolean isAlgorithmValid(String algorithm) {
|
||||
return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
|
||||
}
|
||||
|
||||
public void setAlgorithm(String algorithm) throws KeyInfoException {
|
||||
if (algorithm.startsWith("Hmac")) {
|
||||
algorithm = algorithm.substring(4);
|
||||
}
|
||||
algorithm = algorithm.toUpperCase();
|
||||
|
||||
if (!isAlgorithmValid(algorithm)) {
|
||||
throw new KeyInfoException(String.format("unsupported algorithm: %s", algorithm));
|
||||
}
|
||||
_algorithm = algorithm;
|
||||
}
|
||||
|
||||
public boolean isDigitsValid(int digits) {
|
||||
return digits == 6 || digits == 8;
|
||||
}
|
||||
|
||||
public void setDigits(int digits) throws KeyInfoException {
|
||||
if (!isDigitsValid(digits)) {
|
||||
throw new KeyInfoException(String.format("unsupported amount of digits: %d", digits));
|
||||
}
|
||||
_digits = digits;
|
||||
}
|
||||
|
||||
public boolean isCounterValid(long count) {
|
||||
return count >= 0;
|
||||
}
|
||||
|
||||
public void setCounter(long count) throws KeyInfoException {
|
||||
if (!isCounterValid(count)) {
|
||||
throw new KeyInfoException(String.format("bad count: %d", count));
|
||||
}
|
||||
_counter = count;
|
||||
}
|
||||
|
||||
public boolean isPeriodValid(int period) {
|
||||
return period > 0;
|
||||
}
|
||||
|
||||
public void setPeriod(int period) throws KeyInfoException {
|
||||
if (!isPeriodValid(period)) {
|
||||
throw new KeyInfoException(String.format("bad period: %d", period));
|
||||
}
|
||||
_period = period;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package me.impy.aegis.crypto;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
|
@ -51,11 +50,11 @@ public class KeyStoreHandle {
|
|||
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_ECB)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setRandomizedEncryptionRequired(false)
|
||||
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
|
||||
.setRandomizedEncryptionRequired(true)
|
||||
.setKeySize(CryptoUtils.CRYPTO_AEAD_KEY_SIZE * 8)
|
||||
.build());
|
||||
|
||||
return generator.generateKey();
|
||||
|
@ -81,8 +80,7 @@ public class KeyStoreHandle {
|
|||
// and see if KeyPermanentlyInvalidatedException is thrown
|
||||
if (isSupported()) {
|
||||
try {
|
||||
@SuppressLint("GetInstance")
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (KeyPermanentlyInvalidatedException e) {
|
||||
return null;
|
||||
|
|
|
@ -28,7 +28,7 @@ public class MasterKey implements Serializable {
|
|||
|
||||
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
|
||||
try {
|
||||
Cipher cipher = CryptoUtils.createCipher(_key, Cipher.ENCRYPT_MODE);
|
||||
Cipher cipher = CryptoUtils.createEncryptCipher(_key);
|
||||
return CryptoUtils.encrypt(bytes, cipher);
|
||||
} catch (NoSuchPaddingException
|
||||
| NoSuchAlgorithmException
|
||||
|
@ -42,7 +42,7 @@ public class MasterKey implements Serializable {
|
|||
|
||||
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
|
||||
try {
|
||||
Cipher cipher = CryptoUtils.createCipher(_key, Cipher.DECRYPT_MODE, params.Nonce);
|
||||
Cipher cipher = CryptoUtils.createDecryptCipher(_key, params.getNonce());
|
||||
return CryptoUtils.decrypt(bytes, cipher, params);
|
||||
} catch (NoSuchPaddingException
|
||||
| NoSuchAlgorithmException
|
||||
|
@ -55,10 +55,6 @@ public class MasterKey implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
public byte[] getHash() {
|
||||
return CryptoUtils.hashKey(_key);
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return _key.getEncoded();
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
package me.impy.aegis.crypto.otp;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
|
||||
public class OTP {
|
||||
private OTP() {
|
||||
}
|
||||
|
||||
public static String generateOTP(KeyInfo info) throws OTPException {
|
||||
String otp;
|
||||
|
||||
try {
|
||||
switch (info.getType()) {
|
||||
case "totp":
|
||||
String time = Long.toHexString(System.currentTimeMillis() / 1000 / info.getPeriod());
|
||||
otp = TOTP.generateTOTP(info.getSecret(), time, info.getDigits(), info.getAlgorithm(true));
|
||||
break;
|
||||
case "hotp":
|
||||
otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Bad OTP type");
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new OTPException(e);
|
||||
}
|
||||
|
||||
return otp;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package me.impy.aegis.crypto.otp;
|
||||
|
||||
public class OTPException extends Exception {
|
||||
public OTPException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -4,19 +4,16 @@ import org.json.JSONArray;
|
|||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
|
||||
public class Database {
|
||||
private static final int VERSION = 1;
|
||||
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
private DatabaseEntryList _entries = new DatabaseEntryList();
|
||||
|
||||
public JSONObject serialize() throws DatabaseException {
|
||||
public JSONObject serialize() {
|
||||
try {
|
||||
JSONArray array = new JSONArray();
|
||||
for (DatabaseEntry e : _entries) {
|
||||
|
@ -28,7 +25,7 @@ public class Database {
|
|||
obj.put("entries", array);
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,52 +41,30 @@ public class Database {
|
|||
for (int i = 0; i < array.length(); i++) {
|
||||
DatabaseEntry entry = new DatabaseEntry(null);
|
||||
entry.deserialize(array.getJSONObject(i));
|
||||
addKey(entry);
|
||||
addEntry(entry);
|
||||
}
|
||||
} catch (JSONException | KeyInfoException e) {
|
||||
} catch (OtpInfoException | JSONException e) {
|
||||
throw new DatabaseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addKey(DatabaseEntry entry) {
|
||||
if (tryGetKeyByUUID(entry.getUUID()) != null) {
|
||||
throw new AssertionError("entry found with the same uuid");
|
||||
}
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void removeKey(DatabaseEntry entry) {
|
||||
entry = getKeyByUUID(entry.getUUID());
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replaceKey(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = getKeyByUUID(newEntry.getUUID());
|
||||
_entries.set(_entries.indexOf(oldEntry), newEntry);
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
_entries.replace(newEntry);
|
||||
}
|
||||
|
||||
public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_entries.swap(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getKeys() {
|
||||
return Collections.unmodifiableList(_entries);
|
||||
}
|
||||
|
||||
private DatabaseEntry tryGetKeyByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseEntry getKeyByUUID(UUID uuid) {
|
||||
DatabaseEntry entry = tryGetKeyByUUID(uuid);
|
||||
if (entry == null) {
|
||||
throw new AssertionError("no entry found with the same uuid");
|
||||
}
|
||||
return entry;
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
return _entries.getList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,33 +6,44 @@ import org.json.JSONObject;
|
|||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
|
||||
public class DatabaseEntry implements Serializable {
|
||||
private UUID _uuid;
|
||||
private String _name = "";
|
||||
private String _issuer = "";
|
||||
private String _icon = "";
|
||||
private KeyInfo _info;
|
||||
private OtpInfo _info;
|
||||
|
||||
public DatabaseEntry() {
|
||||
this(new KeyInfo());
|
||||
}
|
||||
|
||||
public DatabaseEntry(KeyInfo info) {
|
||||
public DatabaseEntry(OtpInfo info) {
|
||||
_info = info;
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
public JSONObject serialize() throws JSONException {
|
||||
public DatabaseEntry(OtpInfo info, String name, String issuer) {
|
||||
this(info);
|
||||
setName(name);
|
||||
setIssuer(issuer);
|
||||
}
|
||||
|
||||
public JSONObject serialize() {
|
||||
JSONObject obj = new JSONObject();
|
||||
|
||||
try {
|
||||
obj.put("type", _info.getType());
|
||||
obj.put("uuid", _uuid.toString());
|
||||
obj.put("name", _name);
|
||||
obj.put("url", _info.getURL());
|
||||
obj.put("issuer", _issuer);
|
||||
obj.put("info", _info.toJson());
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public void deserialize(JSONObject obj) throws JSONException, KeyInfoException {
|
||||
public void deserialize(JSONObject obj) throws JSONException, OtpInfoException {
|
||||
// if there is no uuid, generate a new one
|
||||
if (!obj.has("uuid")) {
|
||||
_uuid = UUID.randomUUID();
|
||||
|
@ -40,29 +51,43 @@ public class DatabaseEntry implements Serializable {
|
|||
_uuid = UUID.fromString(obj.getString("uuid"));
|
||||
}
|
||||
_name = obj.getString("name");
|
||||
_info = KeyInfo.fromURL(obj.getString("url"));
|
||||
_issuer = obj.getString("issuer");
|
||||
_info = OtpInfo.parseJson(obj.getString("type"), obj.getJSONObject("info"));
|
||||
}
|
||||
|
||||
public UUID getUUID() {
|
||||
return _uuid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return _issuer;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return _icon;
|
||||
}
|
||||
public KeyInfo getInfo() {
|
||||
|
||||
public OtpInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
_issuer = issuer;
|
||||
}
|
||||
|
||||
public void setIcon(String icon) {
|
||||
_icon = icon;
|
||||
}
|
||||
public void setInfo(KeyInfo info) {
|
||||
|
||||
public void setInfo(OtpInfo info) {
|
||||
_info = info;
|
||||
}
|
||||
}
|
||||
|
|
62
app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java
Normal file
62
app/src/main/java/me/impy/aegis/db/DatabaseEntryList.java
Normal file
|
@ -0,0 +1,62 @@
|
|||
package me.impy.aegis.db;
|
||||
|
||||
import android.support.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 (tryGetByUUID(entry.getUUID()) != null) {
|
||||
throw new AssertionError("entry found with the same uuid");
|
||||
}
|
||||
_entries.add(entry);
|
||||
}
|
||||
|
||||
public void remove(DatabaseEntry entry) {
|
||||
entry = getByUUID(entry.getUUID());
|
||||
_entries.remove(entry);
|
||||
}
|
||||
|
||||
public void replace(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = getByUUID(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);
|
||||
}
|
||||
|
||||
private DatabaseEntry tryGetByUUID(UUID uuid) {
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
if (entry.getUUID().equals(uuid)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DatabaseEntry getByUUID(UUID uuid) {
|
||||
DatabaseEntry entry = tryGetByUUID(uuid);
|
||||
if (entry == null) {
|
||||
throw new AssertionError("no entry found with the same uuid");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package me.impy.aegis.db;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
@ -9,11 +10,10 @@ import me.impy.aegis.crypto.CryptParameters;
|
|||
import me.impy.aegis.crypto.CryptResult;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.crypto.MasterKeyException;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotCollectionException;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.slots.SlotListException;
|
||||
import me.impy.aegis.encoding.Base64;
|
||||
import me.impy.aegis.encoding.Base64Exception;
|
||||
import me.impy.aegis.encoding.Hex;
|
||||
import me.impy.aegis.encoding.HexException;
|
||||
|
||||
public class DatabaseFile {
|
||||
|
@ -21,22 +21,15 @@ public class DatabaseFile {
|
|||
|
||||
private Object _content;
|
||||
private CryptParameters _cryptParameters;
|
||||
private SlotCollection _slots;
|
||||
private SlotList _slots;
|
||||
|
||||
public byte[] serialize() throws DatabaseFileException {
|
||||
public byte[] serialize() {
|
||||
try {
|
||||
JSONObject cryptObj = null;
|
||||
if (isEncrypted()) {
|
||||
cryptObj = new JSONObject();
|
||||
cryptObj.put("nonce", Hex.encode(_cryptParameters.Nonce));
|
||||
cryptObj.put("tag", Hex.encode(_cryptParameters.Tag));
|
||||
}
|
||||
|
||||
// don't write the crypt parameters if the content is not encrypted
|
||||
boolean plain = _content instanceof JSONObject || _slots == null || cryptObj == null;
|
||||
// don't write the crypt parameters and slots if the content is not encrypted
|
||||
boolean plain = _content instanceof JSONObject || !isEncrypted();
|
||||
JSONObject headerObj = new JSONObject();
|
||||
headerObj.put("slots", plain ? JSONObject.NULL : SlotCollection.serialize(_slots));
|
||||
headerObj.put("params", plain ? JSONObject.NULL : cryptObj);
|
||||
headerObj.put("slots", plain ? JSONObject.NULL : SlotList.serialize(_slots));
|
||||
headerObj.put("params", plain ? JSONObject.NULL : _cryptParameters.toJson());
|
||||
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("version", VERSION);
|
||||
|
@ -45,8 +38,8 @@ public class DatabaseFile {
|
|||
|
||||
String string = obj.toString(4);
|
||||
return string.getBytes("UTF-8");
|
||||
} catch (SlotCollectionException | UnsupportedEncodingException | JSONException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
} catch (JSONException | UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,17 +51,14 @@ public class DatabaseFile {
|
|||
throw new DatabaseFileException("unsupported version");
|
||||
}
|
||||
|
||||
JSONObject slotObj = headerObj.optJSONObject("slots");
|
||||
JSONArray slotObj = headerObj.optJSONArray("slots");
|
||||
if (slotObj != null) {
|
||||
_slots = SlotCollection.deserialize(slotObj);
|
||||
_slots = SlotList.deserialize(slotObj);
|
||||
}
|
||||
|
||||
JSONObject cryptObj = headerObj.optJSONObject("params");
|
||||
if (cryptObj != null) {
|
||||
_cryptParameters = new CryptParameters() {{
|
||||
Nonce = Hex.decode(cryptObj.getString("nonce"));
|
||||
Tag = Hex.decode(cryptObj.getString("tag"));
|
||||
}};
|
||||
_cryptParameters = CryptParameters.parseJson(cryptObj);
|
||||
}
|
||||
|
||||
if (cryptObj == null || slotObj == null) {
|
||||
|
@ -76,7 +66,7 @@ public class DatabaseFile {
|
|||
} else {
|
||||
_content = obj.getString("db");
|
||||
}
|
||||
} catch (SlotCollectionException | UnsupportedEncodingException | JSONException | HexException e) {
|
||||
} catch (SlotListException | UnsupportedEncodingException | JSONException | HexException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +83,7 @@ public class DatabaseFile {
|
|||
try {
|
||||
byte[] bytes = Base64.decode((String) _content);
|
||||
CryptResult result = key.decrypt(bytes, _cryptParameters);
|
||||
return new JSONObject(new String(result.Data, "UTF-8"));
|
||||
return new JSONObject(new String(result.getData(), "UTF-8"));
|
||||
} catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
|
||||
throw new DatabaseFileException(e);
|
||||
}
|
||||
|
@ -111,18 +101,18 @@ public class DatabaseFile {
|
|||
byte[] dbBytes = string.getBytes("UTF-8");
|
||||
|
||||
CryptResult result = key.encrypt(dbBytes);
|
||||
_content = Base64.encode(result.Data);
|
||||
_cryptParameters = result.Parameters;
|
||||
_content = Base64.encode(result.getData());
|
||||
_cryptParameters = result.getParams();
|
||||
} catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
|
||||
throw new DatabaseFileException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SlotCollection getSlots() {
|
||||
public SlotList getSlots() {
|
||||
return _slots;
|
||||
}
|
||||
|
||||
public void setSlots(SlotCollection slots) {
|
||||
public void setSlots(SlotList slots) {
|
||||
_slots = slots;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import java.util.List;
|
|||
|
||||
import me.impy.aegis.BuildConfig;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
|
||||
public class DatabaseManager {
|
||||
private static final String FILENAME = "aegis.json";
|
||||
|
@ -103,7 +103,7 @@ public class DatabaseManager {
|
|||
stream.close();
|
||||
}
|
||||
}
|
||||
} catch (IOException | DatabaseFileException e) {
|
||||
} catch (IOException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ public class DatabaseManager {
|
|||
_file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
} catch (DatabaseException | DatabaseFileException e) {
|
||||
} catch (DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
@ -157,34 +157,34 @@ public class DatabaseManager {
|
|||
}
|
||||
|
||||
return file.getAbsolutePath();
|
||||
} catch (DatabaseException | IOException | DatabaseFileException e) {
|
||||
} catch (IOException | DatabaseFileException e) {
|
||||
throw new DatabaseManagerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addKey(DatabaseEntry entry) {
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.addKey(entry);
|
||||
_db.addEntry(entry);
|
||||
}
|
||||
|
||||
public void removeKey(DatabaseEntry entry) {
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.removeKey(entry);
|
||||
_db.removeEntry(entry);
|
||||
}
|
||||
|
||||
public void replaceKey(DatabaseEntry entry) {
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
assertState(false, true);
|
||||
_db.replaceKey(entry);
|
||||
_db.replaceEntry(entry);
|
||||
}
|
||||
|
||||
public void swapKeys(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
assertState(false, true);
|
||||
_db.swapKeys(entry1, entry2);
|
||||
_db.swapEntries(entry1, entry2);
|
||||
}
|
||||
|
||||
public List<DatabaseEntry> getKeys() {
|
||||
public List<DatabaseEntry> getEntries() {
|
||||
assertState(false, true);
|
||||
return _db.getKeys();
|
||||
return _db.getEntries();
|
||||
}
|
||||
|
||||
public MasterKey getMasterKey() {
|
||||
|
@ -196,7 +196,7 @@ public class DatabaseManager {
|
|||
return _file;
|
||||
}
|
||||
|
||||
public void enableEncryption(MasterKey key, SlotCollection slots) {
|
||||
public void enableEncryption(MasterKey key, SlotList slots) {
|
||||
assertState(false, true);
|
||||
_key = key;
|
||||
_file.setSlots(slots);
|
||||
|
|
|
@ -3,9 +3,6 @@ package me.impy.aegis.db.slots;
|
|||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
|
@ -23,7 +20,7 @@ public class PasswordSlot extends RawSlot {
|
|||
}
|
||||
|
||||
@Override
|
||||
public JSONObject serialize() throws SlotException {
|
||||
public JSONObject serialize() {
|
||||
try {
|
||||
JSONObject obj = super.serialize();
|
||||
obj.put("n", _n);
|
||||
|
@ -32,7 +29,7 @@ public class PasswordSlot extends RawSlot {
|
|||
obj.put("salt", Hex.encode(_salt));
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new SlotException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
package me.impy.aegis.db.slots;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
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.AEADBadTagException;
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
|
@ -17,6 +18,8 @@ import javax.crypto.NoSuchPaddingException;
|
|||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import me.impy.aegis.crypto.CryptParameters;
|
||||
import me.impy.aegis.crypto.CryptResult;
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.encoding.Hex;
|
||||
|
@ -29,53 +32,70 @@ public abstract class Slot implements Serializable {
|
|||
|
||||
protected UUID _uuid;
|
||||
protected byte[] _encryptedMasterKey;
|
||||
protected CryptParameters _encryptedMasterKeyParams;
|
||||
|
||||
protected Slot() {
|
||||
_uuid = UUID.randomUUID();
|
||||
}
|
||||
|
||||
// getKey decrypts the encrypted master key in this slot with the given key and returns it.
|
||||
public SecretKey getKey(Cipher cipher) throws SlotException {
|
||||
// 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 {
|
||||
byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey);
|
||||
return new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_AEAD);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
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 with the given key and stores the result in this slot.
|
||||
// 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();
|
||||
_encryptedMasterKey = cipher.doFinal(masterKeyBytes);
|
||||
CryptResult res = CryptoUtils.encrypt(masterKeyBytes, cipher);
|
||||
_encryptedMasterKey = res.getData();
|
||||
_encryptedMasterKeyParams = res.getParams();
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new SlotException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// suppress the AES ECB warning
|
||||
// this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it
|
||||
@SuppressLint("getInstance")
|
||||
public static Cipher createCipher(SecretKey key, int mode) throws SlotException {
|
||||
public static Cipher createEncryptCipher(SecretKey key) throws SlotException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW);
|
||||
cipher.init(mode, key);
|
||||
return cipher;
|
||||
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
return CryptoUtils.createEncryptCipher(key);
|
||||
} catch (InvalidAlgorithmParameterException
|
||||
| NoSuchPaddingException
|
||||
| NoSuchAlgorithmException
|
||||
| InvalidKeyException e) {
|
||||
throw new SlotException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject serialize() throws SlotException {
|
||||
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 serialize() {
|
||||
try {
|
||||
JSONObject obj = new JSONObject();
|
||||
JSONObject paramObj = _encryptedMasterKeyParams.toJson();
|
||||
obj.put("type", getType());
|
||||
obj.put("uuid", _uuid.toString());
|
||||
obj.put("key", Hex.encode(_encryptedMasterKey));
|
||||
obj.put("key_params", paramObj);
|
||||
return obj;
|
||||
} catch (JSONException e) {
|
||||
throw new SlotException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,13 +104,17 @@ public abstract class Slot implements Serializable {
|
|||
if (obj.getInt("type") != getType()) {
|
||||
throw new SlotException("slot type mismatch");
|
||||
}
|
||||
|
||||
// if there is no uuid, generate a new one
|
||||
if (!obj.has("uuid")) {
|
||||
_uuid = UUID.randomUUID();
|
||||
} else {
|
||||
_uuid = UUID.fromString(obj.getString("uuid"));
|
||||
}
|
||||
|
||||
JSONObject paramObj = obj.getJSONObject("key_params");
|
||||
_encryptedMasterKey = Hex.decode(obj.getString("key"));
|
||||
_encryptedMasterKeyParams = CryptParameters.parseJson(paramObj);
|
||||
} catch (JSONException | HexException e) {
|
||||
throw new SlotException(e);
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
package me.impy.aegis.db.slots;
|
||||
|
||||
public class SlotCollectionException extends Exception {
|
||||
public SlotCollectionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public SlotCollectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,11 @@
|
|||
package me.impy.aegis.db.slots;
|
||||
|
||||
public class SlotIntegrityException extends Exception {
|
||||
public SlotIntegrityException() {
|
||||
|
||||
}
|
||||
|
||||
public SlotIntegrityException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,47 +6,28 @@ import org.json.JSONObject;
|
|||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.encoding.Hex;
|
||||
import me.impy.aegis.encoding.HexException;
|
||||
|
||||
public class SlotCollection implements Iterable<Slot>, Serializable {
|
||||
public class SlotList implements Iterable<Slot>, Serializable {
|
||||
private List<Slot> _slots = new ArrayList<>();
|
||||
private byte[] _masterHash;
|
||||
|
||||
public static JSONObject serialize(SlotCollection slots) throws SlotCollectionException {
|
||||
try {
|
||||
JSONObject obj = new JSONObject();
|
||||
obj.put("hash", Hex.encode(slots.getMasterHash()));
|
||||
|
||||
JSONArray entries = new JSONArray();
|
||||
public static JSONArray serialize(SlotList slots) {
|
||||
JSONArray array = new JSONArray();
|
||||
for (Slot slot : slots) {
|
||||
entries.put(slot.serialize());
|
||||
array.put(slot.serialize());
|
||||
}
|
||||
|
||||
obj.put("entries", entries);
|
||||
return obj;
|
||||
} catch (SlotException | JSONException e) {
|
||||
throw new SlotCollectionException(e);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
public static SlotCollection deserialize(JSONObject obj) throws SlotCollectionException {
|
||||
SlotCollection slots = new SlotCollection();
|
||||
public static SlotList deserialize(JSONArray array) throws SlotListException {
|
||||
SlotList slots = new SlotList();
|
||||
|
||||
try {
|
||||
byte[] masterHash = Hex.decode(obj.getString("hash"));
|
||||
slots.setMasterHash(masterHash);
|
||||
|
||||
JSONArray entries = obj.getJSONArray("entries");
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
Slot slot;
|
||||
JSONObject slotObj = entries.getJSONObject(i);
|
||||
JSONObject slotObj = array.getJSONObject(i);
|
||||
|
||||
switch (slotObj.getInt("type")) {
|
||||
case Slot.TYPE_RAW:
|
||||
|
@ -65,8 +46,8 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
|
|||
slot.deserialize(slotObj);
|
||||
slots.add(slot);
|
||||
}
|
||||
} catch (SlotException | JSONException | HexException e) {
|
||||
throw new SlotCollectionException(e);
|
||||
} catch (SlotException | JSONException e) {
|
||||
throw new SlotListException(e);
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
@ -116,26 +97,4 @@ public class SlotCollection implements Iterable<Slot>, Serializable {
|
|||
public Iterator<Slot> iterator() {
|
||||
return _slots.iterator();
|
||||
}
|
||||
|
||||
public void encrypt(Slot slot, MasterKey key, Cipher cipher) throws SlotException {
|
||||
slot.setKey(key, cipher);
|
||||
setMasterHash(key.getHash());
|
||||
}
|
||||
|
||||
public MasterKey decrypt(Slot slot, Cipher cipher) throws SlotException, SlotIntegrityException {
|
||||
byte[] hash = getMasterHash();
|
||||
MasterKey key = new MasterKey(slot.getKey(cipher));
|
||||
if (!Arrays.equals(hash, key.getHash())) {
|
||||
throw new SlotIntegrityException();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private void setMasterHash(byte[] masterHash) {
|
||||
_masterHash = masterHash;
|
||||
}
|
||||
|
||||
private byte[] getMasterHash() {
|
||||
return _masterHash;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package me.impy.aegis.db.slots;
|
||||
|
||||
public class SlotListException extends Exception {
|
||||
public SlotListException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public SlotListException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -24,8 +24,6 @@ package me.impy.aegis.encoding;
|
|||
|
||||
import java.util.Arrays;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
|
||||
/**
|
||||
* Base32 - encodes and decodes RFC3548 Base32
|
||||
* (see http://www.faqs.org/rfcs/rfc3548.html )
|
||||
|
|
|
@ -5,8 +5,6 @@ import android.widget.EditText;
|
|||
|
||||
import java.util.Arrays;
|
||||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
|
||||
public class EditTextHelper {
|
||||
private EditTextHelper() {
|
||||
}
|
||||
|
|
|
@ -8,13 +8,16 @@ public class TextDrawableHelper {
|
|||
|
||||
}
|
||||
|
||||
public static TextDrawable generate(String s) {
|
||||
if (s == null || s.length() <= 1) {
|
||||
public static TextDrawable generate(String text, String fallback) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
if (fallback == null || fallback.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
text = fallback;
|
||||
}
|
||||
|
||||
ColorGenerator generator = ColorGenerator.MATERIAL;
|
||||
int color = generator.getColor(s);
|
||||
return TextDrawable.builder().buildRound(s.substring(0, 1).toUpperCase(), color);
|
||||
int color = generator.getColor(text);
|
||||
return TextDrawable.builder().buildRound(text.substring(0, 1).toUpperCase(), color);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ package me.impy.aegis.helpers;
|
|||
|
||||
import android.os.Handler;
|
||||
|
||||
public class UIRefresher {
|
||||
public class UiRefresher {
|
||||
private boolean _running;
|
||||
private Listener _listener;
|
||||
private Handler _handler;
|
||||
|
||||
public UIRefresher(Listener listener) {
|
||||
public UiRefresher(Listener listener) {
|
||||
_listener = listener;
|
||||
_handler = new Handler();
|
||||
}
|
|
@ -43,7 +43,7 @@ public class AegisImporter extends DatabaseImporter {
|
|||
|
||||
Database db = new Database();
|
||||
db.deserialize(obj);
|
||||
return db.getKeys();
|
||||
return db.getEntries();
|
||||
} catch (DatabaseException | DatabaseFileException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
|
|
@ -8,17 +8,19 @@ import java.io.UnsupportedEncodingException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
import me.impy.aegis.util.ByteInputStream;
|
||||
|
||||
public class AndOTPImporter extends DatabaseImporter {
|
||||
public class AndOtpImporter extends DatabaseImporter {
|
||||
private JSONArray _obj;
|
||||
|
||||
public AndOTPImporter(ByteInputStream stream) {
|
||||
public AndOtpImporter(ByteInputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
|
@ -41,30 +43,35 @@ public class AndOTPImporter extends DatabaseImporter {
|
|||
for (int i = 0; i < _obj.length(); i++) {
|
||||
JSONObject obj = _obj.getJSONObject(i);
|
||||
|
||||
KeyInfo key = new KeyInfo();
|
||||
key.setAlgorithm(obj.getString("algorithm"));
|
||||
key.setDigits(obj.getInt("digits"));
|
||||
key.setPeriod(obj.getInt("period"));
|
||||
key.setType(obj.getString("type"));
|
||||
if (key.getType().equals("hotp")) {
|
||||
key.setCounter(obj.getLong("counter"));
|
||||
String type = obj.getString("type");
|
||||
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) {
|
||||
key.setIssuer(parts[0]);
|
||||
key.setAccountName(parts[1]);
|
||||
issuer = parts[0];
|
||||
name = parts[1];
|
||||
} else {
|
||||
key.setAccountName(parts[0]);
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
|
||||
key.setSecret(secret);
|
||||
|
||||
DatabaseEntry entry = new DatabaseEntry(key);
|
||||
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
|
||||
entries.add(entry);
|
||||
}
|
||||
} catch (Base32Exception | KeyInfoException | JSONException e) {
|
||||
} catch (Base32Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
|
@ -16,8 +16,8 @@ public abstract class DatabaseImporter {
|
|||
// 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.put("andOTP", AndOtpImporter.class);
|
||||
importers.put("FreeOTP", FreeOtpImporter.class);
|
||||
_importers = Collections.unmodifiableMap(importers);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,4 +4,8 @@ public class DatabaseImporterException extends Exception {
|
|||
public DatabaseImporterException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public DatabaseImporterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,47 +12,86 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
import me.impy.aegis.util.ByteInputStream;
|
||||
|
||||
public class FreeOTPImporter extends DatabaseImporter {
|
||||
public FreeOTPImporter(ByteInputStream stream) {
|
||||
public class FreeOtpImporter extends DatabaseImporter {
|
||||
private List<XmlEntry> _xmlEntries;
|
||||
|
||||
public FreeOtpImporter(ByteInputStream stream) {
|
||||
super(stream);
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
private static class XmlEntry {
|
||||
String Name;
|
||||
String Value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse() throws DatabaseImporterException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseEntry> convert() throws DatabaseImporterException {
|
||||
try {
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(_stream, null);
|
||||
parser.nextTag();
|
||||
return parse(parser);
|
||||
} catch (KeyInfoException | XmlPullParserException | JSONException | IOException e) {
|
||||
_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");
|
||||
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<DatabaseEntry> parse(XmlPullParser parser)
|
||||
throws IOException, XmlPullParserException, JSONException, KeyInfoException {
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
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) {
|
||||
|
@ -68,32 +107,7 @@ public class FreeOTPImporter extends DatabaseImporter {
|
|||
entries.add(parseEntry(parser));
|
||||
}
|
||||
|
||||
List<DatabaseEntry> profiles = new ArrayList<>();
|
||||
|
||||
for (Entry entry : entries) {
|
||||
if (entry.Name.equals("tokenOrder")) {
|
||||
// TODO: order
|
||||
JSONArray array = new JSONArray(entry.Value);
|
||||
} else {
|
||||
JSONObject obj = new JSONObject(entry.Value);
|
||||
|
||||
KeyInfo key = new KeyInfo();
|
||||
key.setAlgorithm(obj.getString("algo"));
|
||||
key.setCounter(obj.getLong("counter"));
|
||||
key.setDigits(obj.getInt("digits"));
|
||||
key.setIssuer(obj.getString("issuerExt"));
|
||||
key.setAccountName(obj.optString("label"));
|
||||
key.setPeriod(obj.getInt("period"));
|
||||
key.setType(obj.getString("type"));
|
||||
byte[] secret = toBytes(obj.getJSONArray("secret"));
|
||||
key.setSecret(secret);
|
||||
|
||||
DatabaseEntry profile = new DatabaseEntry(key);
|
||||
profiles.add(profile);
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static byte[] toBytes(JSONArray array) throws JSONException {
|
||||
|
@ -104,12 +118,16 @@ public class FreeOTPImporter extends DatabaseImporter {
|
|||
return bytes;
|
||||
}
|
||||
|
||||
private static Entry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
|
||||
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");
|
||||
return new Entry() {{ Name = name; Value = value; }};
|
||||
|
||||
XmlEntry entry = new XmlEntry();
|
||||
entry.Name = name;
|
||||
entry.Value = value;
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
|
149
app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java
Normal file
149
app/src/main/java/me/impy/aegis/otp/GoogleAuthInfo.java
Normal file
|
@ -0,0 +1,149 @@
|
|||
package me.impy.aegis.otp;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.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(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;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
package me.impy.aegis.crypto;
|
||||
package me.impy.aegis.otp;
|
||||
|
||||
public class KeyInfoException extends Exception {
|
||||
public KeyInfoException(String message) {
|
||||
public class GoogleAuthInfoException extends Exception {
|
||||
public GoogleAuthInfoException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public GoogleAuthInfoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public KeyInfoException(String message, Throwable cause) {
|
||||
public GoogleAuthInfoException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
67
app/src/main/java/me/impy/aegis/otp/HotpInfo.java
Normal file
67
app/src/main/java/me/impy/aegis/otp/HotpInfo.java
Normal file
|
@ -0,0 +1,67 @@
|
|||
package me.impy.aegis.otp;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import me.impy.aegis.crypto.otp.HOTP;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
116
app/src/main/java/me/impy/aegis/otp/OtpInfo.java
Normal file
116
app/src/main/java/me/impy/aegis/otp/OtpInfo.java
Normal file
|
@ -0,0 +1,116 @@
|
|||
package me.impy.aegis.otp;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
|
||||
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 parseJson(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;
|
||||
}
|
||||
}
|
11
app/src/main/java/me/impy/aegis/otp/OtpInfoException.java
Normal file
11
app/src/main/java/me/impy/aegis/otp/OtpInfoException.java
Normal file
|
@ -0,0 +1,11 @@
|
|||
package me.impy.aegis.otp;
|
||||
|
||||
public class OtpInfoException extends Exception {
|
||||
public OtpInfoException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public OtpInfoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
66
app/src/main/java/me/impy/aegis/otp/TotpInfo.java
Normal file
66
app/src/main/java/me/impy/aegis/otp/TotpInfo.java
Normal file
|
@ -0,0 +1,66 @@
|
|||
package me.impy.aegis.otp;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import me.impy.aegis.crypto.otp.TOTP;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -26,17 +26,17 @@ import me.impy.aegis.crypto.MasterKey;
|
|||
import me.impy.aegis.db.slots.FingerprintSlot;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.Slot;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
import me.impy.aegis.helpers.FingerprintHelper;
|
||||
import me.impy.aegis.helpers.FingerprintUiHelper;
|
||||
import me.impy.aegis.helpers.EditTextHelper;
|
||||
import me.impy.aegis.ui.tasks.SlotCollectionTask;
|
||||
import me.impy.aegis.ui.tasks.SlotListTask;
|
||||
|
||||
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotCollectionTask.Callback {
|
||||
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
|
||||
private EditText _textPassword;
|
||||
|
||||
private SlotCollection _slots;
|
||||
private SlotList _slots;
|
||||
private FingerprintUiHelper _fingerHelper;
|
||||
private Cipher _fingerCipher;
|
||||
|
||||
|
@ -57,7 +57,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
|||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
_slots = (SlotCollection) intent.getSerializableExtra("slots");
|
||||
_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);
|
||||
|
@ -75,7 +75,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
|||
invalidated = true;
|
||||
continue;
|
||||
}
|
||||
_fingerCipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
|
||||
_fingerCipher = slot.createDecryptCipher(key);
|
||||
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
|
||||
boxFingerprint.setVisibility(View.VISIBLE);
|
||||
invalidated = false;
|
||||
|
@ -112,10 +112,8 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
|
|||
}
|
||||
|
||||
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
|
||||
new SlotCollectionTask<>(type, this, this).execute(new SlotCollectionTask.Params(){{
|
||||
Slots = _slots;
|
||||
Obj = obj;
|
||||
}});
|
||||
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
|
||||
new SlotListTask<>(type, this, this).execute(params);
|
||||
}
|
||||
|
||||
private void setKey(MasterKey key) {
|
||||
|
|
|
@ -19,32 +19,39 @@ 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 me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.encoding.Base32;
|
||||
import me.impy.aegis.encoding.Base32Exception;
|
||||
import me.impy.aegis.helpers.EditTextHelper;
|
||||
import me.impy.aegis.helpers.SpinnerHelper;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
import me.impy.aegis.ui.dialogs.Dialogs;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
|
||||
public class EditProfileActivity extends AegisActivity {
|
||||
public class EditEntryActivity extends AegisActivity {
|
||||
private boolean _isNew = false;
|
||||
private boolean _edited = false;
|
||||
private KeyProfile _profile;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
private ImageView _iconView;
|
||||
|
||||
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;
|
||||
|
@ -56,27 +63,28 @@ public class EditProfileActivity extends AegisActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_profile);
|
||||
setContentView(R.layout.activity_edit_entry);
|
||||
|
||||
ActionBar bar = getSupportActionBar();
|
||||
bar.setHomeAsUpIndicator(R.drawable.ic_close);
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// if the intent doesn't contain a KeyProfile, create a new one
|
||||
// retrieve info from the calling activity
|
||||
Intent intent = getIntent();
|
||||
_profile = (KeyProfile) intent.getSerializableExtra("KeyProfile");
|
||||
_entry = (DatabaseEntry) intent.getSerializableExtra("entry");
|
||||
_isNew = intent.getBooleanExtra("isNew", false);
|
||||
if (_profile == null) {
|
||||
_profile = new KeyProfile();
|
||||
}
|
||||
if (_isNew) {
|
||||
setTitle("Add profile");
|
||||
}
|
||||
|
||||
// set up fields
|
||||
_iconView = findViewById(R.id.profile_drawable);
|
||||
_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);
|
||||
|
@ -84,37 +92,84 @@ public class EditProfileActivity extends AegisActivity {
|
|||
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
|
||||
_spinnerDigits = findViewById(R.id.spinner_digits);
|
||||
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
|
||||
|
||||
_advancedSettingsHeader = findViewById(R.id.accordian_header);
|
||||
_advancedSettings = findViewById(R.id.expandableLayout);
|
||||
|
||||
updateFields();
|
||||
// fill the fields with values if possible
|
||||
if (_entry != null) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName());
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_textName.setText(_entry.getName());
|
||||
_textIssuer.setText(_entry.getIssuer());
|
||||
|
||||
OtpInfo info = _entry.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 = _entry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = _entry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
|
||||
|
||||
String algo = _entry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String digits = Integer.toString(_entry.getInfo().getDigits());
|
||||
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
|
||||
}
|
||||
|
||||
// listen for changes to any of the fields
|
||||
_textName.addTextChangedListener(_textListener);
|
||||
_textIssuer.addTextChangedListener(_textListener);
|
||||
_textPeriod.addTextChangedListener(_textListener);
|
||||
_textCounter.addTextChangedListener(_textListener);
|
||||
_textSecret.addTextChangedListener(_textListener);
|
||||
_spinnerType.setOnTouchListener(_selectedListener);
|
||||
_spinnerType.setOnItemSelectedListener(_selectedListener);
|
||||
_spinnerAlgo.setOnTouchListener(_selectedListener);
|
||||
_spinnerAlgo.setOnItemSelectedListener(_selectedListener);
|
||||
_spinnerDigits.setOnTouchListener(_selectedListener);
|
||||
_spinnerDigits.setOnItemSelectedListener(_selectedListener);
|
||||
|
||||
// update the icon if the text changed
|
||||
_textName.addTextChangedListener(new TextWatcher() {
|
||||
_textIssuer.addTextChangedListener(_iconChangeListener);
|
||||
_textName.addTextChangedListener(_iconChangeListener);
|
||||
|
||||
// show/hide period and counter fields on type change
|
||||
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
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();
|
||||
}
|
||||
|
||||
_selectedListener.onItemSelected(parent, view, position, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
}
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(s.toString());
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -122,36 +177,12 @@ public class EditProfileActivity extends AegisActivity {
|
|||
openAdvancedSettings();
|
||||
});
|
||||
|
||||
// Automatically open advanced settings since 'Secret' is required.
|
||||
// automatically open advanced settings since 'Secret' is required.
|
||||
if(_isNew){
|
||||
openAdvancedSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFields() {
|
||||
DatabaseEntry entry = _profile.getEntry();
|
||||
_iconView.setImageDrawable(_profile.getDrawable());
|
||||
|
||||
_textName.setText(entry.getName());
|
||||
_textIssuer.setText(entry.getInfo().getIssuer());
|
||||
_textPeriod.setText(Integer.toString(entry.getInfo().getPeriod()));
|
||||
|
||||
byte[] secretBytes = entry.getInfo().getSecret();
|
||||
if (secretBytes != null) {
|
||||
char[] secretChars = Base32.encode(secretBytes);
|
||||
_textSecret.setText(secretChars, 0, secretChars.length);
|
||||
}
|
||||
|
||||
String type = entry.getInfo().getType();
|
||||
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
|
||||
|
||||
String algo = entry.getInfo().getAlgorithm(false);
|
||||
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
|
||||
|
||||
String digits = Integer.toString(entry.getInfo().getDigits());
|
||||
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPreferredTheme(boolean darkMode) {
|
||||
if (darkMode) {
|
||||
|
@ -252,7 +283,7 @@ public class EditProfileActivity extends AegisActivity {
|
|||
|
||||
private void finish(boolean delete) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("KeyProfile", _profile);
|
||||
intent.putExtra("entry", _entry);
|
||||
intent.putExtra("delete", delete);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
@ -264,14 +295,6 @@ public class EditProfileActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
int period;
|
||||
try {
|
||||
period = Integer.parseInt(_textPeriod.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
onError("Period is not an integer.");
|
||||
return false;
|
||||
}
|
||||
|
||||
String type = _spinnerType.getSelectedItem().toString();
|
||||
String algo = _spinnerAlgo.getSelectedItem().toString();
|
||||
|
||||
|
@ -283,24 +306,60 @@ public class EditProfileActivity extends AegisActivity {
|
|||
return false;
|
||||
}
|
||||
|
||||
DatabaseEntry entry = _profile.getEntry();
|
||||
entry.setName(_textName.getText().toString());
|
||||
KeyInfo info = entry.getInfo();
|
||||
|
||||
byte[] secret;
|
||||
try {
|
||||
char[] secret = EditTextHelper.getEditTextChars(_textSecret);
|
||||
info.setSecret(secret);
|
||||
info.setIssuer(_textIssuer.getText().toString());
|
||||
info.setPeriod(period);
|
||||
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
|
||||
} catch (Base32Exception e) {
|
||||
onError("Secret is not valid base32.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// set otp info
|
||||
OtpInfo info;
|
||||
try {
|
||||
switch (type.toLowerCase()) {
|
||||
case "totp":
|
||||
int period;
|
||||
try {
|
||||
period = Integer.parseInt(_textPeriod.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
onError("Period is not an integer.");
|
||||
return false;
|
||||
}
|
||||
info = new TotpInfo(secret, algo, digits, period);
|
||||
break;
|
||||
case "hotp":
|
||||
long counter;
|
||||
try {
|
||||
counter = Long.parseLong(_textCounter.getText().toString());
|
||||
} catch (NumberFormatException e) {
|
||||
onError("Counter is not an integer.");
|
||||
return false;
|
||||
}
|
||||
info = new HotpInfo(secret, algo, digits, counter);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
info.setDigits(digits);
|
||||
info.setAlgorithm(algo);
|
||||
info.setType(type);
|
||||
info.setAccountName(_textName.getText().toString());
|
||||
} catch (KeyInfoException e) {
|
||||
} catch (OtpInfoException e) {
|
||||
onError("The entered info is incorrect: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// set database entry info
|
||||
DatabaseEntry entry = _entry;
|
||||
if (entry == null) {
|
||||
entry = new DatabaseEntry(info);
|
||||
} else {
|
||||
entry.setInfo(info);
|
||||
}
|
||||
entry.setIssuer(_textIssuer.getText().toString());
|
||||
entry.setName(_textName.getText().toString());
|
||||
|
||||
_entry = entry;
|
||||
finish(false);
|
||||
return true;
|
||||
}
|
||||
|
@ -334,6 +393,22 @@ public class EditProfileActivity extends AegisActivity {
|
|||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_textIssuer.getText().toString(), _textName.getText().toString());
|
||||
_iconView.setImageDrawable(drawable);
|
||||
}
|
||||
};
|
||||
|
||||
private class SpinnerItemSelectedListener implements AdapterView.OnItemSelectedListener, View.OnTouchListener {
|
||||
private boolean _userSelect = false;
|
||||
|
|
@ -18,13 +18,12 @@ import javax.crypto.SecretKey;
|
|||
import me.impy.aegis.Preferences;
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.MasterKey;
|
||||
import me.impy.aegis.db.DatabaseException;
|
||||
import me.impy.aegis.db.DatabaseFileException;
|
||||
import me.impy.aegis.db.DatabaseManagerException;
|
||||
import me.impy.aegis.db.slots.FingerprintSlot;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.Slot;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.Database;
|
||||
import me.impy.aegis.db.DatabaseFile;
|
||||
import me.impy.aegis.db.DatabaseManager;
|
||||
|
@ -105,10 +104,8 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
|
||||
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
|
||||
_passwordSlot = new PasswordSlot();
|
||||
new DerivationTask(this, this).execute(new DerivationTask.Params() {{
|
||||
Slot = _passwordSlot;
|
||||
Password = _authenticatedSlide.getPassword();
|
||||
}});
|
||||
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) {
|
||||
|
@ -135,7 +132,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
masterKey = MasterKey.generate();
|
||||
}
|
||||
|
||||
SlotCollection slots = null;
|
||||
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
|
||||
|
@ -143,8 +140,8 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
throw new RuntimeException();
|
||||
}
|
||||
try {
|
||||
slots = new SlotCollection();
|
||||
slots.encrypt(_passwordSlot, masterKey, _passwordCipher);
|
||||
_passwordSlot.setKey(masterKey, _passwordCipher);
|
||||
slots = new SlotList();
|
||||
slots.add(_passwordSlot);
|
||||
_databaseFile.setSlots(slots);
|
||||
} catch (SlotException e) {
|
||||
|
@ -158,7 +155,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
// and add it to the list of slots
|
||||
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
|
||||
Cipher cipher = _authenticatedSlide.getFingerCipher();
|
||||
slots.encrypt(slot, masterKey, cipher);
|
||||
slot.setKey(masterKey, cipher);
|
||||
slots.add(slot);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
|
@ -175,7 +172,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
_databaseFile.setContent(obj, masterKey);
|
||||
}
|
||||
DatabaseManager.save(getApplicationContext(), _databaseFile);
|
||||
} catch (DatabaseException | DatabaseManagerException | DatabaseFileException e) {
|
||||
} catch (DatabaseManagerException | DatabaseFileException e) {
|
||||
setException(e);
|
||||
return;
|
||||
}
|
||||
|
@ -194,7 +191,7 @@ public class IntroActivity extends AppIntro implements DerivationTask.Callback {
|
|||
public void onTaskFinished(SecretKey key) {
|
||||
if (key != null) {
|
||||
try {
|
||||
_passwordCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
||||
_passwordCipher = Slot.createEncryptCipher(key);
|
||||
} catch (SlotException e) {
|
||||
setException(e);
|
||||
}
|
||||
|
|
|
@ -26,15 +26,14 @@ import me.impy.aegis.db.DatabaseEntry;
|
|||
import me.impy.aegis.db.DatabaseManager;
|
||||
import me.impy.aegis.helpers.PermissionHelper;
|
||||
import me.impy.aegis.ui.dialogs.Dialogs;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
import me.impy.aegis.ui.views.KeyProfileView;
|
||||
import me.impy.aegis.ui.views.EntryListView;
|
||||
|
||||
public class MainActivity extends AegisActivity implements KeyProfileView.Listener {
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
private static final int CODE_SCAN_KEYINFO = 0;
|
||||
private static final int CODE_ADD_KEYINFO = 1;
|
||||
private static final int CODE_EDIT_KEYINFO = 2;
|
||||
private static final int CODE_ENTER_KEYINFO = 3;
|
||||
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;
|
||||
|
@ -44,7 +43,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
private AegisApplication _app;
|
||||
private DatabaseManager _db;
|
||||
private KeyProfileView _keyProfileView;
|
||||
private EntryListView _entryListView;
|
||||
|
||||
private Menu _menu;
|
||||
private FloatingActionsMenu _fabMenu;
|
||||
|
@ -58,20 +57,20 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
// set up the main view
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
// set up the key profile view
|
||||
_keyProfileView = (KeyProfileView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_keyProfileView.setListener(this);
|
||||
_keyProfileView.setShowIssuer(getPreferences().isIssuerVisible());
|
||||
// set up the entry view
|
||||
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
|
||||
_entryListView.setListener(this);
|
||||
_entryListView.setShowIssuer(getPreferences().isIssuerVisible());
|
||||
|
||||
// set up the floating action button
|
||||
_fabMenu = findViewById(R.id.fab);
|
||||
findViewById(R.id.fab_enter).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
onEnterKeyInfo();
|
||||
onEnterEntry();
|
||||
});
|
||||
findViewById(R.id.fab_scan).setOnClickListener(view -> {
|
||||
_fabMenu.collapse();
|
||||
onScanKeyInfo();
|
||||
onScan();
|
||||
});
|
||||
|
||||
// skip this part if this is the not initial startup and the database has been unlocked
|
||||
|
@ -101,9 +100,9 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
}
|
||||
|
||||
// if the database has been decrypted at this point, we can load the key profiles
|
||||
// if the database has been decrypted at this point, we can load the entries
|
||||
if (!_db.isLocked()) {
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,17 +140,17 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
|
||||
switch (requestCode) {
|
||||
case CODE_SCAN_KEYINFO:
|
||||
onScanKeyInfoResult(resultCode, data);
|
||||
case CODE_SCAN:
|
||||
onScanResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ADD_KEYINFO:
|
||||
onAddKeyInfoResult(resultCode, data);
|
||||
case CODE_ADD_ENTRY:
|
||||
onAddEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_EDIT_KEYINFO:
|
||||
onEditKeyInfoResult(resultCode, data);
|
||||
case CODE_EDIT_ENTRY:
|
||||
onEditEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_ENTER_KEYINFO:
|
||||
onEnterKeyInfoResult(resultCode, data);
|
||||
case CODE_ENTER_ENTRY:
|
||||
onEnterEntryResult(resultCode, data);
|
||||
break;
|
||||
case CODE_DO_INTRO:
|
||||
onDoIntroResult(resultCode, data);
|
||||
|
@ -174,35 +173,35 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
|
||||
switch (requestCode) {
|
||||
case CODE_PERM_CAMERA:
|
||||
onScanKeyInfo();
|
||||
onScan();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onPreferencesResult(int resultCode, Intent data) {
|
||||
// refresh the entire key profile list if needed
|
||||
// refresh the entire entry list if needed
|
||||
if (data.getBooleanExtra("needsRecreate", false)) {
|
||||
recreate();
|
||||
} else if (data.getBooleanExtra("needsRefresh", false)) {
|
||||
boolean showIssuer = getPreferences().isIssuerVisible();
|
||||
_keyProfileView.setShowIssuer(showIssuer);
|
||||
_entryListView.setShowIssuer(showIssuer);
|
||||
}
|
||||
}
|
||||
|
||||
private void startEditProfileActivity(int requestCode, KeyProfile profile, boolean isNew) {
|
||||
Intent intent = new Intent(this, EditProfileActivity.class);
|
||||
if (profile != null) {
|
||||
intent.putExtra("KeyProfile", profile);
|
||||
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);
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
private void onEnterKeyInfo() {
|
||||
startEditProfileActivity(CODE_ENTER_KEYINFO, null, true);
|
||||
private void onEnterEntry() {
|
||||
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
|
||||
}
|
||||
|
||||
private void onScanKeyInfo() {
|
||||
private void onScan() {
|
||||
if (!PermissionHelper.request(this, CODE_PERM_CAMERA, Manifest.permission.CAMERA)) {
|
||||
return;
|
||||
}
|
||||
|
@ -210,49 +209,47 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
startScanActivity();
|
||||
}
|
||||
|
||||
private void onScanKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onScanResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile)data.getSerializableExtra("KeyProfile");
|
||||
startEditProfileActivity(CODE_ADD_KEYINFO, profile, true);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onAddEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
addKey(profile);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEditKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onEditEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
if (!data.getBooleanExtra("delete", false)) {
|
||||
// this profile has been serialized/deserialized and is no longer the same instance it once was
|
||||
// to deal with this, the replaceKey functions are used
|
||||
_db.replaceKey(profile.getEntry());
|
||||
_keyProfileView.replaceKey(profile);
|
||||
// to deal with this, the replaceEntry functions are used
|
||||
_db.replaceEntry(entry);
|
||||
_entryListView.replaceEntry(entry);
|
||||
saveDatabase();
|
||||
} else {
|
||||
deleteProfile(profile);
|
||||
deleteEntry(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onEnterKeyInfoResult(int resultCode, Intent data) {
|
||||
private void onEnterEntryResult(int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
|
||||
addKey(profile);
|
||||
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
|
||||
addEntry(entry);
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
private void addKey(KeyProfile profile) {
|
||||
DatabaseEntry entry = profile.getEntry();
|
||||
entry.setName(entry.getInfo().getAccountName());
|
||||
_db.addKey(entry);
|
||||
_keyProfileView.addKey(profile);
|
||||
private void addEntry(DatabaseEntry entry) {
|
||||
_db.addEntry(entry);
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
|
||||
private void onDoIntroResult(int resultCode, Intent data) {
|
||||
|
@ -275,7 +272,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
return;
|
||||
}
|
||||
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
}
|
||||
|
||||
private void onDecryptResult(int resultCode, Intent intent) {
|
||||
|
@ -289,13 +286,13 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
return;
|
||||
}
|
||||
|
||||
loadKeyProfiles();
|
||||
loadEntries();
|
||||
doShortcutActions();
|
||||
}
|
||||
|
||||
private void startScanActivity() {
|
||||
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN_KEYINFO);
|
||||
startActivityForResult(scannerActivity, CODE_SCAN);
|
||||
}
|
||||
|
||||
private boolean doShortcutActions() {
|
||||
|
@ -330,12 +327,12 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
updateLockIcon();
|
||||
|
||||
// refresh all codes to prevent showing old ones
|
||||
_keyProfileView.refresh();
|
||||
_entryListView.refresh();
|
||||
}
|
||||
|
||||
private BottomSheetDialog createBottomSheet(final KeyProfile profile) {
|
||||
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(this);
|
||||
dialog.setContentView(R.layout.bottom_sheet_edit_profile);
|
||||
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
|
||||
dialog.setCancelable(true);
|
||||
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
|
@ -344,7 +341,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("text/plain", profile.getCode());
|
||||
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(this, "Code copied to the clipboard", Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
@ -352,23 +349,23 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
|
||||
deleteProfile(profile);
|
||||
deleteEntry(entry);
|
||||
});
|
||||
});
|
||||
|
||||
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
|
||||
dialog.dismiss();
|
||||
startEditProfileActivity(CODE_EDIT_KEYINFO, profile, false);
|
||||
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void deleteProfile(KeyProfile profile) {
|
||||
_db.removeKey(profile.getEntry());
|
||||
private void deleteEntry(DatabaseEntry entry) {
|
||||
_db.removeEntry(entry);
|
||||
saveDatabase();
|
||||
|
||||
_keyProfileView.removeKey(profile);
|
||||
_entryListView.removeEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -387,7 +384,7 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
startActivityForResult(intent, CODE_PREFERENCES);
|
||||
return true;
|
||||
case R.id.action_lock:
|
||||
_keyProfileView.clearKeys();
|
||||
_entryListView.clearEntries();
|
||||
_db.lock();
|
||||
startAuthActivity();
|
||||
return true;
|
||||
|
@ -411,11 +408,11 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
}
|
||||
|
||||
private void loadKeyProfiles() {
|
||||
private void loadEntries() {
|
||||
updateLockIcon();
|
||||
|
||||
for (DatabaseEntry entry : _db.getKeys()) {
|
||||
_keyProfileView.addKey(new KeyProfile(entry));
|
||||
for (DatabaseEntry entry : _db.getEntries()) {
|
||||
_entryListView.addEntry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,17 +425,22 @@ public class MainActivity extends AegisActivity implements KeyProfileView.Listen
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onEntryClick(KeyProfile profile) {
|
||||
createBottomSheet(profile).show();
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
createBottomSheet(entry).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_db.swapKeys(entry1, entry2);
|
||||
_db.swapEntries(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryDrop(DatabaseEntry entry) {
|
||||
saveDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(DatabaseEntry entry) {
|
||||
saveDatabase();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import me.impy.aegis.db.DatabaseEntry;
|
|||
import me.impy.aegis.db.DatabaseManager;
|
||||
import me.impy.aegis.db.DatabaseManagerException;
|
||||
import me.impy.aegis.db.slots.Slot;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
import me.impy.aegis.helpers.PermissionHelper;
|
||||
import me.impy.aegis.importers.AegisImporter;
|
||||
|
@ -313,7 +313,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
|
|||
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
|
||||
List<DatabaseEntry> entries = importer.convert();
|
||||
for (DatabaseEntry entry : entries) {
|
||||
_db.addKey(entry);
|
||||
_db.addEntry(entry);
|
||||
}
|
||||
|
||||
if (!saveDatabase()) {
|
||||
|
@ -369,7 +369,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
|
|||
return;
|
||||
}
|
||||
|
||||
SlotCollection slots = (SlotCollection) data.getSerializableExtra("slots");
|
||||
SlotList slots = (SlotList) data.getSerializableExtra("slots");
|
||||
_db.getFile().setSlots(slots);
|
||||
saveDatabase();
|
||||
}
|
||||
|
@ -390,9 +390,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat implements Pas
|
|||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
MasterKey masterKey = MasterKey.generate();
|
||||
|
||||
SlotCollection slots = new SlotCollection();
|
||||
SlotList slots = new SlotList();
|
||||
try {
|
||||
slots.encrypt(slot, masterKey, cipher);
|
||||
slot.setKey(masterKey, cipher);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
|
|
|
@ -3,6 +3,7 @@ package me.impy.aegis.ui;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -16,11 +17,10 @@ import java.util.Collections;
|
|||
import me.dm7.barcodescanner.core.IViewFinder;
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView;
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.crypto.KeyInfoException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.SquareFinderView;
|
||||
import me.impy.aegis.ui.views.KeyProfile;
|
||||
import me.impy.aegis.otp.GoogleAuthInfo;
|
||||
import me.impy.aegis.otp.GoogleAuthInfoException;
|
||||
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
|
@ -107,16 +107,19 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
|
|||
@Override
|
||||
public void handleResult(Result rawResult) {
|
||||
try {
|
||||
KeyInfo info = KeyInfo.fromURL(rawResult.getText());
|
||||
KeyProfile profile = new KeyProfile(new DatabaseEntry(info));
|
||||
profile.getEntry().setName(info.getAccountName());
|
||||
// parse google auth uri
|
||||
Uri uri = Uri.parse(rawResult.getText());
|
||||
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra("KeyProfile", profile);
|
||||
DatabaseEntry entry = new DatabaseEntry(info.getOtpInfo());
|
||||
entry.setIssuer(info.getIssuer());
|
||||
entry.setName(info.getAccountName());
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("entry", entry);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
} catch (KeyInfoException e) {
|
||||
} catch (GoogleAuthInfoException e) {
|
||||
Toast.makeText(this, "An error occurred while trying to parse the QR code contents", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import me.impy.aegis.crypto.MasterKey;
|
|||
import me.impy.aegis.db.slots.FingerprintSlot;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.Slot;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
import me.impy.aegis.helpers.FingerprintHelper;
|
||||
import me.impy.aegis.ui.dialogs.Dialogs;
|
||||
|
@ -31,7 +31,7 @@ import me.impy.aegis.ui.dialogs.SlotDialogFragment;
|
|||
|
||||
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, SlotDialogFragment.Listener {
|
||||
private MasterKey _masterKey;
|
||||
private SlotCollection _slots;
|
||||
private SlotList _slots;
|
||||
private SlotAdapter _adapter;
|
||||
|
||||
private boolean _edited = false;
|
||||
|
@ -65,7 +65,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
|||
|
||||
// load the slots and masterKey
|
||||
_masterKey = (MasterKey) getIntent().getSerializableExtra("masterKey");
|
||||
_slots = (SlotCollection) getIntent().getSerializableExtra("slots");
|
||||
_slots = (SlotList) getIntent().getSerializableExtra("slots");
|
||||
for (Slot slot : _slots) {
|
||||
_adapter.addSlot(slot);
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
|
|||
@Override
|
||||
public void onSlotResult(Slot slot, Cipher cipher) {
|
||||
try {
|
||||
_slots.encrypt(slot, _masterKey, cipher);
|
||||
slot.setKey(_masterKey, cipher);
|
||||
} catch (SlotException e) {
|
||||
onException(e);
|
||||
return;
|
||||
|
|
|
@ -38,7 +38,7 @@ public class FingerprintDialogFragment extends SlotDialogFragment implements Fin
|
|||
try {
|
||||
_slot = new FingerprintSlot();
|
||||
SecretKey key = new KeyStoreHandle().generateKey(_slot.getUUID().toString());
|
||||
_cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
||||
_cipher = Slot.createEncryptCipher(key);
|
||||
_helper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
|
||||
} catch (KeyStoreHandleException | SlotException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -51,7 +51,7 @@ public class PasswordDialogFragment extends SlotDialogFragment {
|
|||
DerivationTask task = new DerivationTask(getActivity(), key -> {
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
||||
cipher = Slot.createEncryptCipher(key);
|
||||
} catch (SlotException e) {
|
||||
getListener().onException(e);
|
||||
dialog.cancel();
|
||||
|
@ -60,10 +60,7 @@ public class PasswordDialogFragment extends SlotDialogFragment {
|
|||
getListener().onSlotResult(slot, cipher);
|
||||
dialog.dismiss();
|
||||
});
|
||||
task.execute(new DerivationTask.Params() {{
|
||||
Slot = slot;
|
||||
Password = password;
|
||||
}});
|
||||
task.execute(new DerivationTask.Params(slot, password));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
|
|||
}
|
||||
|
||||
try {
|
||||
_fingerCipher = Slot.createCipher(key, Cipher.ENCRYPT_MODE);
|
||||
_fingerCipher = Slot.createEncryptCipher(key);
|
||||
} catch (Exception e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import javax.crypto.SecretKey;
|
|||
|
||||
import me.impy.aegis.crypto.CryptoUtils;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
|
||||
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
|
||||
private Callback _cb;
|
||||
|
@ -22,7 +21,7 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
|
||||
Params params = args[0];
|
||||
byte[] salt = CryptoUtils.generateSalt();
|
||||
return params.Slot.deriveKey(params.Password, salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p);
|
||||
return params.getSlot().deriveKey(params.getPassword(), salt, CryptoUtils.CRYPTO_SCRYPT_N, CryptoUtils.CRYPTO_SCRYPT_r, CryptoUtils.CRYPTO_SCRYPT_p);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -32,8 +31,21 @@ public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, Se
|
|||
}
|
||||
|
||||
public static class Params {
|
||||
public PasswordSlot Slot;
|
||||
public char[] Password;
|
||||
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 {
|
||||
|
|
|
@ -2,8 +2,6 @@ package me.impy.aegis.ui.tasks;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
|
@ -11,45 +9,48 @@ import me.impy.aegis.crypto.MasterKey;
|
|||
import me.impy.aegis.db.slots.FingerprintSlot;
|
||||
import me.impy.aegis.db.slots.PasswordSlot;
|
||||
import me.impy.aegis.db.slots.Slot;
|
||||
import me.impy.aegis.db.slots.SlotCollection;
|
||||
import me.impy.aegis.db.slots.SlotList;
|
||||
import me.impy.aegis.db.slots.SlotException;
|
||||
import me.impy.aegis.db.slots.SlotIntegrityException;
|
||||
|
||||
public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotCollectionTask.Params, MasterKey> {
|
||||
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
|
||||
private Callback _cb;
|
||||
private Class<T> _type;
|
||||
|
||||
public SlotCollectionTask(Class<T> type, Context context, Callback cb) {
|
||||
public SlotListTask(Class<T> type, Context context, Callback cb) {
|
||||
super(context, "Decrypting database");
|
||||
_cb = cb;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MasterKey doInBackground(SlotCollectionTask.Params... args) {
|
||||
protected MasterKey doInBackground(SlotListTask.Params... args) {
|
||||
setPriority();
|
||||
|
||||
Params params = args[0];
|
||||
SlotList slots = params.getSlots();
|
||||
try {
|
||||
if (!params.Slots.has(_type)) {
|
||||
if (!slots.has(_type)) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
MasterKey masterKey = null;
|
||||
for (Slot slot : params.Slots.findAll(_type)) {
|
||||
for (Slot slot : slots.findAll(_type)) {
|
||||
try {
|
||||
if (slot instanceof PasswordSlot) {
|
||||
char[] password = (char[])params.Obj;
|
||||
char[] password = (char[])params.getObj();
|
||||
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
|
||||
Cipher cipher = Slot.createCipher(key, Cipher.DECRYPT_MODE);
|
||||
masterKey = params.Slots.decrypt(slot, cipher);
|
||||
Cipher cipher = slot.createDecryptCipher(key);
|
||||
masterKey = slot.getKey(cipher);
|
||||
} else if (slot instanceof FingerprintSlot) {
|
||||
masterKey = params.Slots.decrypt(slot, (Cipher)params.Obj);
|
||||
masterKey = slot.getKey((Cipher)params.getObj());
|
||||
} else {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
break;
|
||||
} catch (SlotIntegrityException e) { }
|
||||
} catch (SlotIntegrityException e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (masterKey == null) {
|
||||
|
@ -60,7 +61,7 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
|
|||
} catch (SlotIntegrityException e) {
|
||||
return null;
|
||||
} catch (SlotException e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,8 +72,21 @@ public class SlotCollectionTask<T extends Slot> extends ProgressDialogTask<SlotC
|
|||
}
|
||||
|
||||
public static class Params {
|
||||
public SlotCollection Slots;
|
||||
public Object Obj;
|
||||
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 {
|
188
app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java
Normal file
188
app/src/main/java/me/impy/aegis/ui/views/EntryAdapter.java
Normal file
|
@ -0,0 +1,188 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
|
||||
private List<DatabaseEntry> _entries;
|
||||
private static Listener _listener;
|
||||
private boolean _showIssuer;
|
||||
|
||||
public EntryAdapter(Listener listener) {
|
||||
_entries = new ArrayList<>();
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
_showIssuer = showIssuer;
|
||||
}
|
||||
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_entries.add(entry);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
entry = getEntryByUUID(entry.getUUID());
|
||||
int position = _entries.indexOf(entry);
|
||||
_entries.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void clearEntries() {
|
||||
_entries.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void replaceEntry(DatabaseEntry newEntry) {
|
||||
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
|
||||
int position = _entries.indexOf(oldEntry);
|
||||
_entries.set(position, newEntry);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) {
|
||||
_listener.onEntryDrop(_entries.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// notify the database first
|
||||
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
|
||||
|
||||
// update our side of things
|
||||
Collections.swap(_entries, 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();
|
||||
super.onViewRecycled(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final EntryHolder holder, int position) {
|
||||
DatabaseEntry entry = _entries.get(position);
|
||||
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
|
||||
holder.setData(entry, _showIssuer, showProgress);
|
||||
if (showProgress) {
|
||||
holder.startRefreshLoop();
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
_listener.onEntryClick(_entries.get(position));
|
||||
}
|
||||
});
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
return _listener.onLongEntryClick(_entries.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
List<TotpInfo> infos = new ArrayList<>();
|
||||
for (DatabaseEntry entry : _entries) {
|
||||
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 _entries.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);
|
||||
}
|
||||
}
|
|
@ -9,31 +9,38 @@ import android.widget.TextView;
|
|||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.helpers.UIRefresher;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
import me.impy.aegis.helpers.UiRefresher;
|
||||
import me.impy.aegis.otp.HotpInfo;
|
||||
import me.impy.aegis.otp.OtpInfoException;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private TextView _profileName;
|
||||
private TextView _profileCode;
|
||||
private TextView _profileIssuer;
|
||||
private ImageView _profileDrawable;
|
||||
private KeyProfile _profile;
|
||||
private DatabaseEntry _entry;
|
||||
private ImageView _buttonRefresh;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
|
||||
private UIRefresher _refresher;
|
||||
private UiRefresher _refresher;
|
||||
|
||||
public KeyProfileHolder(final View view) {
|
||||
public EntryHolder(final View view) {
|
||||
super(view);
|
||||
_profileName = view.findViewById(R.id.profile_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() {
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
refreshCode();
|
||||
|
@ -42,31 +49,39 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return _profile.getEntry().getInfo().getMillisTillNextRotation();
|
||||
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setData(KeyProfile profile, boolean showIssuer, boolean showProgress) {
|
||||
_profile = profile;
|
||||
public void setData(DatabaseEntry entry, boolean showIssuer, boolean showProgress) {
|
||||
_entry = entry;
|
||||
|
||||
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.INVISIBLE);
|
||||
// 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(profile.getEntry().getInfo().getPeriod());
|
||||
_progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
|
||||
}
|
||||
|
||||
_profileName.setText(profile.getEntry().getName());
|
||||
// only show the button if this entry is of type HotpInfo
|
||||
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
|
||||
|
||||
_profileName.setText(entry.getName());
|
||||
_profileIssuer.setText("");
|
||||
if (showIssuer) {
|
||||
_profileIssuer.setText(" - " + profile.getEntry().getInfo().getIssuer());
|
||||
_profileIssuer.setText(" - " + entry.getIssuer());
|
||||
}
|
||||
|
||||
TextDrawable drawable = profile.getDrawable();
|
||||
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName());
|
||||
_profileDrawable.setImageDrawable(drawable);
|
||||
|
||||
refreshCode();
|
||||
}
|
||||
|
||||
public void setOnRefreshClickListener(View.OnClickListener listener) {
|
||||
_buttonRefresh.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void startRefreshLoop() {
|
||||
_refresher.start();
|
||||
}
|
||||
|
@ -75,8 +90,8 @@ public class KeyProfileHolder extends RecyclerView.ViewHolder {
|
|||
_refresher.stop();
|
||||
}
|
||||
|
||||
private void refreshCode() {
|
||||
String otp = _profile.refreshCode();
|
||||
public void refreshCode() {
|
||||
String otp = _entry.getInfo().getOtp();
|
||||
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
|
||||
}
|
||||
}
|
|
@ -11,29 +11,29 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
|
||||
import me.impy.aegis.helpers.UIRefresher;
|
||||
import me.impy.aegis.helpers.UiRefresher;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listener {
|
||||
private KeyProfileAdapter _adapter;
|
||||
public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
private EntryAdapter _adapter;
|
||||
private Listener _listener;
|
||||
|
||||
private PeriodProgressBar _progressBar;
|
||||
private boolean _showProgress = false;
|
||||
|
||||
private UIRefresher _refresher;
|
||||
private UiRefresher _refresher;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
_adapter = new KeyProfileAdapter(this);
|
||||
_adapter = new EntryAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_keyprofile_view, container, false);
|
||||
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);
|
||||
|
@ -48,7 +48,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
touchHelper.attachToRecyclerView(rvKeyProfiles);
|
||||
rvKeyProfiles.setAdapter(_adapter);
|
||||
|
||||
_refresher = new UIRefresher(new UIRefresher.Listener() {
|
||||
_refresher = new UiRefresher(new UiRefresher.Listener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
refresh();
|
||||
|
@ -56,7 +56,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
|
||||
@Override
|
||||
public long getMillisTillNextRefresh() {
|
||||
return KeyInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
|
||||
return TotpInfo.getMillisTillNextRotation(_adapter.getUniformPeriod());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -82,7 +82,7 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
_progressBar.setPeriod(_adapter.getUniformPeriod());
|
||||
startRefreshLoop();
|
||||
} else {
|
||||
_progressBar.setVisibility(View.INVISIBLE);
|
||||
_progressBar.setVisibility(View.GONE);
|
||||
stopRefreshLoop();
|
||||
}
|
||||
}
|
||||
|
@ -101,23 +101,28 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileClick(KeyProfile profile) {
|
||||
_listener.onEntryClick(profile);
|
||||
public void onEntryClick(DatabaseEntry entry) {
|
||||
_listener.onEntryClick(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongKeyProfileClick(KeyProfile profile) {
|
||||
public boolean onLongEntryClick(DatabaseEntry entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2) {
|
||||
_listener.onEntryMove(profile1.getEntry(), profile2.getEntry());
|
||||
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
|
||||
_listener.onEntryMove(entry1, entry2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyProfileDrop(KeyProfile profile) {
|
||||
_listener.onEntryDrop(profile.getEntry());
|
||||
public void onEntryDrop(DatabaseEntry entry) {
|
||||
_listener.onEntryDrop(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntryChange(DatabaseEntry entry) {
|
||||
_listener.onEntryChange(entry);
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
|
@ -125,29 +130,30 @@ public class KeyProfileView extends Fragment implements KeyProfileAdapter.Listen
|
|||
_adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addKey(KeyProfile profile) {
|
||||
_adapter.addKey(profile);
|
||||
public void addEntry(DatabaseEntry entry) {
|
||||
_adapter.addEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void removeKey(KeyProfile profile) {
|
||||
_adapter.removeKey(profile);
|
||||
public void removeEntry(DatabaseEntry entry) {
|
||||
_adapter.removeEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void clearKeys() {
|
||||
_adapter.clearKeys();
|
||||
public void clearEntries() {
|
||||
_adapter.clearEntries();
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public void replaceKey(KeyProfile profile) {
|
||||
_adapter.replaceKey(profile);
|
||||
public void replaceEntry(DatabaseEntry entry) {
|
||||
_adapter.replaceEntry(entry);
|
||||
checkPeriodUniformity();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEntryClick(KeyProfile profile);
|
||||
void onEntryClick(DatabaseEntry entry);
|
||||
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
|
||||
void onEntryDrop(DatabaseEntry entry);
|
||||
void onEntryChange(DatabaseEntry entry);
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
|
||||
import me.impy.aegis.crypto.otp.OTP;
|
||||
import me.impy.aegis.crypto.otp.OTPException;
|
||||
import me.impy.aegis.db.DatabaseEntry;
|
||||
import me.impy.aegis.helpers.TextDrawableHelper;
|
||||
|
||||
public class KeyProfile implements Serializable {
|
||||
private String _code;
|
||||
private DatabaseEntry _entry;
|
||||
|
||||
public KeyProfile() {
|
||||
this(new DatabaseEntry());
|
||||
}
|
||||
|
||||
public KeyProfile(DatabaseEntry entry) {
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
public DatabaseEntry getEntry() {
|
||||
return _entry;
|
||||
}
|
||||
public String getCode() {
|
||||
return _code;
|
||||
}
|
||||
|
||||
public String refreshCode() {
|
||||
try {
|
||||
_code = OTP.generateOTP(_entry.getInfo());
|
||||
} catch (OTPException e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
}
|
||||
return _code;
|
||||
}
|
||||
|
||||
public TextDrawable getDrawable() {
|
||||
return TextDrawableHelper.generate(getEntry().getName());
|
||||
}
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
package me.impy.aegis.ui.views;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import me.impy.aegis.R;
|
||||
import me.impy.aegis.helpers.ItemTouchHelperAdapter;
|
||||
|
||||
public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileHolder> implements ItemTouchHelperAdapter {
|
||||
private ArrayList<KeyProfile> _keyProfiles;
|
||||
private static Listener _listener;
|
||||
private boolean _showIssuer;
|
||||
|
||||
public KeyProfileAdapter(Listener listener) {
|
||||
_keyProfiles = new ArrayList<>();
|
||||
_listener = listener;
|
||||
}
|
||||
|
||||
public void setShowIssuer(boolean showIssuer) {
|
||||
_showIssuer = showIssuer;
|
||||
}
|
||||
|
||||
public void addKey(KeyProfile profile) {
|
||||
_keyProfiles.add(profile);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeKey(KeyProfile profile) {
|
||||
profile = getKeyByUUID(profile.getEntry().getUUID());
|
||||
int position = _keyProfiles.indexOf(profile);
|
||||
_keyProfiles.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void clearKeys() {
|
||||
_keyProfiles.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void replaceKey(KeyProfile newProfile) {
|
||||
KeyProfile oldProfile = getKeyByUUID(newProfile.getEntry().getUUID());
|
||||
int position = _keyProfiles.indexOf(oldProfile);
|
||||
_keyProfiles.set(position, newProfile);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
private KeyProfile getKeyByUUID(UUID uuid) {
|
||||
for (KeyProfile profile : _keyProfiles) {
|
||||
if (profile.getEntry().getUUID().equals(uuid)) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("no key profile found with the same id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDismiss(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemDrop(int position) {
|
||||
_listener.onKeyProfileDrop(_keyProfiles.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemMove(int firstPosition, int secondPosition) {
|
||||
// notify the database first
|
||||
_listener.onKeyProfileMove(_keyProfiles.get(firstPosition), _keyProfiles.get(secondPosition));
|
||||
|
||||
// update our side of things
|
||||
Collections.swap(_keyProfiles, firstPosition, secondPosition);
|
||||
notifyItemMoved(firstPosition, secondPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyProfileHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_keyprofile, parent, false);
|
||||
return new KeyProfileHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(KeyProfileHolder holder) {
|
||||
holder.stopRefreshLoop();
|
||||
super.onViewRecycled(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final KeyProfileHolder holder, int position) {
|
||||
boolean uniform = isPeriodUniform();
|
||||
final KeyProfile profile = _keyProfiles.get(position);
|
||||
holder.setData(profile, _showIssuer, !uniform);
|
||||
if (!uniform) {
|
||||
holder.startRefreshLoop();
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
_listener.onKeyProfileClick(_keyProfiles.get(position));
|
||||
}
|
||||
});
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int position = holder.getAdapterPosition();
|
||||
return _listener.onLongKeyProfileClick(_keyProfiles.get(position));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getUniformPeriod() {
|
||||
if (_keyProfiles.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int period = _keyProfiles.get(0).getEntry().getInfo().getPeriod();
|
||||
for (KeyProfile profile : _keyProfiles) {
|
||||
if (period != profile.getEntry().getInfo().getPeriod()) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return period;
|
||||
}
|
||||
|
||||
public boolean isPeriodUniform() {
|
||||
return getUniformPeriod() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _keyProfiles.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onKeyProfileClick(KeyProfile profile);
|
||||
boolean onLongKeyProfileClick(KeyProfile profile);
|
||||
void onKeyProfileMove(KeyProfile profile1, KeyProfile profile2);
|
||||
void onKeyProfileDrop(KeyProfile profile);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import android.util.AttributeSet;
|
|||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import me.impy.aegis.crypto.KeyInfo;
|
||||
import me.impy.aegis.otp.TotpInfo;
|
||||
|
||||
public class PeriodProgressBar extends ProgressBar {
|
||||
private int _period;
|
||||
|
@ -40,7 +40,7 @@ public class PeriodProgressBar extends ProgressBar {
|
|||
setProgress(maxProgress);
|
||||
|
||||
// calculate the progress the bar should start at
|
||||
long millisTillRotation = KeyInfo.getMillisTillNextRotation(_period);
|
||||
long millisTillRotation = TotpInfo.getMillisTillNextRotation(_period);
|
||||
long period = _period * maxProgress;
|
||||
int currentProgress = maxProgress - (int) ((((double) period - millisTillRotation) / period) * maxProgress);
|
||||
|
||||
|
|
7
app/src/main/res/drawable/ic_counter_black_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_counter_black_24dp.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M4,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M4,6V18H11V6H4M20,18V6H18.76C19,6.54 18.95,7.07 18.95,7.13C18.88,7.8 18.41,8.5 18.24,8.75L15.91,11.3L19.23,11.28L19.24,12.5L14.04,12.47L14,11.47C14,11.47 17.05,8.24 17.2,7.95C17.34,7.67 17.91,6 16.5,6C15.27,6.05 15.41,7.3 15.41,7.3L13.87,7.31C13.87,7.31 13.88,6.65 14.25,6H13V18H15.58L15.57,17.14L16.54,17.13C16.54,17.13 17.45,16.97 17.46,16.08C17.5,15.08 16.65,15.08 16.5,15.08C16.37,15.08 15.43,15.13 15.43,15.95H13.91C13.91,15.95 13.95,13.89 16.5,13.89C19.1,13.89 18.96,15.91 18.96,15.91C18.96,15.91 19,17.16 17.85,17.63L18.37,18H20M8.92,16H7.42V10.2L5.62,10.76V9.53L8.76,8.41H8.92V16Z" />
|
||||
</vector>
|
7
app/src/main/res/drawable/ic_refresh_black_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_refresh_black_24dp.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" />
|
||||
</vector>
|
|
@ -168,6 +168,8 @@
|
|||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:id="@+id/row_period"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
<ImageView android:layout_column="0"
|
||||
|
@ -216,6 +218,41 @@
|
|||
</LinearLayout>
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:id="@+id/row_counter"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
<ImageView android:layout_column="0"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_counter_black_24dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:tint="?attr/iconColorPrimary"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="20dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
<EditText
|
||||
android:id="@+id/text_counter"
|
||||
android:hint="Counter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:backgroundTint="#949494"
|
||||
android:layout_gravity="center"
|
||||
android:inputType="numberDecimal"/>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"/>
|
||||
</LinearLayout>
|
||||
</TableRow>
|
||||
|
||||
<TableRow
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
|
@ -9,7 +9,7 @@
|
|||
tools:context="me.impy.aegis.ui.MainActivity">
|
||||
|
||||
<fragment
|
||||
android:name="me.impy.aegis.ui.views.KeyProfileView"
|
||||
android:name="me.impy.aegis.ui.views.EntryListView"
|
||||
android:id="@+id/key_profiles"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:card_view="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
|
@ -32,8 +30,9 @@
|
|||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_alignParentBottom="false"
|
||||
android:layout_alignParentTop="false"
|
||||
android:layout_alignWithParentIfMissing="false"
|
||||
|
@ -95,6 +94,20 @@
|
|||
android:textStyle="normal|bold"/>
|
||||
|
||||
</RelativeLayout>
|
||||
<ImageView
|
||||
android:id="@+id/buttonRefresh"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:padding="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_refresh_black_24dp"
|
||||
android:tint="?attr/iconColorPrimary"
|
||||
android:background="?android:attr/selectableItemBackground" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
|
@ -2,7 +2,7 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="me.impy.aegis.ui.EditProfileActivity">
|
||||
tools:context="me.impy.aegis.ui.EditEntryActivity">
|
||||
<item
|
||||
android:id="@+id/action_save"
|
||||
app:showAsAction="ifRoom"
|
||||
|
|
|
@ -2,6 +2,9 @@ package me.impy.aegis;
|
|||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import me.impy.aegis.crypto.otp.HOTP;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
@ -22,7 +25,7 @@ public class HOTPTest {
|
|||
};
|
||||
|
||||
@Test
|
||||
public void vectorsMatch() throws Exception {
|
||||
public void vectorsMatch() throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
for (int i = 0; i < _vectors.length; i++) {
|
||||
String otp = HOTP.generateOTP(_secret, i, 6, false, -1);
|
||||
assertEquals(_vectors[i], otp);
|
||||
|
|
|
@ -7,47 +7,42 @@ import me.impy.aegis.crypto.otp.TOTP;
|
|||
import static org.junit.Assert.*;
|
||||
|
||||
public class TOTPTest {
|
||||
private class testVector {
|
||||
public String Time;
|
||||
public String Mode;
|
||||
public String OTP;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc6238#appendix-B
|
||||
private final testVector[] _vectors = {
|
||||
new testVector(){{ Time = "0000000000000001"; OTP = "94287082"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "0000000000000001"; OTP = "46119246"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "0000000000000001"; OTP = "90693936"; Mode = "HmacSHA512"; }},
|
||||
new testVector(){{ Time = "00000000023523EC"; OTP = "07081804"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "00000000023523EC"; OTP = "68084774"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "00000000023523EC"; OTP = "25091201"; Mode = "HmacSHA512"; }},
|
||||
new testVector(){{ Time = "00000000023523ED"; OTP = "14050471"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "00000000023523ED"; OTP = "67062674"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "00000000023523ED"; OTP = "99943326"; Mode = "HmacSHA512"; }},
|
||||
new testVector(){{ Time = "000000000273EF07"; OTP = "89005924"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "000000000273EF07"; OTP = "91819424"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "000000000273EF07"; OTP = "93441116"; Mode = "HmacSHA512"; }},
|
||||
new testVector(){{ Time = "0000000003F940AA"; OTP = "69279037"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "0000000003F940AA"; OTP = "90698825"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "0000000003F940AA"; OTP = "38618901"; Mode = "HmacSHA512"; }},
|
||||
new testVector(){{ Time = "0000000027BC86AA"; OTP = "65353130"; Mode = "HmacSHA1"; }},
|
||||
new testVector(){{ Time = "0000000027BC86AA"; OTP = "77737706"; Mode = "HmacSHA256"; }},
|
||||
new testVector(){{ Time = "0000000027BC86AA"; OTP = "47863826"; Mode = "HmacSHA512"; }}
|
||||
private final String[][] _vectors = {
|
||||
// time, OPT, algorithm
|
||||
{"0000000000000001", "94287082", "HmacSHA1"},
|
||||
{"0000000000000001", "46119246", "HmacSHA256"},
|
||||
{"0000000000000001", "90693936", "HmacSHA512"},
|
||||
{"00000000023523EC", "07081804", "HmacSHA1"},
|
||||
{"00000000023523EC", "68084774", "HmacSHA256"},
|
||||
{"00000000023523EC", "25091201", "HmacSHA512"},
|
||||
{"00000000023523ED", "14050471", "HmacSHA1"},
|
||||
{"00000000023523ED", "67062674", "HmacSHA256"},
|
||||
{"00000000023523ED", "99943326", "HmacSHA512"},
|
||||
{"000000000273EF07", "89005924", "HmacSHA1"},
|
||||
{"000000000273EF07", "91819424", "HmacSHA256"},
|
||||
{"000000000273EF07", "93441116", "HmacSHA512"},
|
||||
{"0000000003F940AA", "69279037", "HmacSHA1"},
|
||||
{"0000000003F940AA", "90698825", "HmacSHA256"},
|
||||
{"0000000003F940AA", "38618901", "HmacSHA512"},
|
||||
{"0000000027BC86AA", "65353130", "HmacSHA1"},
|
||||
{"0000000027BC86AA", "77737706", "HmacSHA256"},
|
||||
{"0000000027BC86AA", "47863826", "HmacSHA512"}
|
||||
};
|
||||
|
||||
private final byte[] _seed = new byte[] {
|
||||
private final byte[] _seed = new byte[]{
|
||||
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30,
|
||||
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30
|
||||
};
|
||||
|
||||
private final byte[] _seed32 = new byte[] {
|
||||
private final byte[] _seed32 = new byte[]{
|
||||
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36,
|
||||
0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34,
|
||||
0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32
|
||||
};
|
||||
|
||||
private final byte[] _seed64 = new byte[] {
|
||||
private final byte[] _seed64 = new byte[]{
|
||||
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36,
|
||||
0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32,
|
||||
0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
|
@ -55,11 +50,11 @@ public class TOTPTest {
|
|||
};
|
||||
|
||||
@Test
|
||||
public void vectorsMatch() throws Exception {
|
||||
for (testVector v : _vectors) {
|
||||
public void vectorsMatch() {
|
||||
for (String[] vector : _vectors) {
|
||||
byte[] seed;
|
||||
|
||||
switch (v.Mode) {
|
||||
switch (vector[2]) {
|
||||
case "HmacSHA1":
|
||||
seed = _seed;
|
||||
break;
|
||||
|
@ -74,8 +69,8 @@ public class TOTPTest {
|
|||
return;
|
||||
}
|
||||
|
||||
String otp = TOTP.generateTOTP(seed, v.Time, 8, v.Mode);
|
||||
assertEquals(v.OTP, otp);
|
||||
String otp = TOTP.generateTOTP(seed, vector[0], 8, vector[2]);
|
||||
assertEquals(vector[1], otp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
31
testdata/aegis_export.json
vendored
31
testdata/aegis_export.json
vendored
File diff suppressed because one or more lines are too long
130
testdata/aegis_export_plain.json
vendored
130
testdata/aegis_export_plain.json
vendored
|
@ -8,54 +8,124 @@
|
|||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "8e986927-a7a3-4d35-88fb-509ae7960155",
|
||||
"name": "Bob",
|
||||
"url": "otpauth:\/\/totp\/Google%3ABob?digits=6&period=30&algorithm=SHA1&secret=KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F&issuer=Google"
|
||||
"issuer": "Google",
|
||||
"info": {
|
||||
"secret": "KD3WHMYQ7K7DGV5QFJNN2ZUATBC44C7F",
|
||||
"algo": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "97155de0-fb6a-4dfa-ac59-2abe54f5d059",
|
||||
"name": "Henk",
|
||||
"url": "otpauth:\/\/totp\/Scaleway%3AHenk?digits=6&period=10&algorithm=SHA256&secret=MBQX47S3XOSOF7YT2CO3LZNACULZNY4P&issuer=Scaleway"
|
||||
},
|
||||
{
|
||||
"uuid": "7dca76af-fa6a-4585-8d47-5a8b78130c9c",
|
||||
"name": "Oh wait, they don't have TOTP",
|
||||
"url": "otpauth:\/\/totp\/Namecheap%3AOh%20wait%2C%20they%20don't%20have%20TOTP?digits=8&period=30&algorithm=SHA512&secret=HIMV4HX3REXMUG236OOLN4GS7URI247Y&issuer=Namecheap"
|
||||
},
|
||||
{
|
||||
"uuid": "435a3542-c566-4cc0-a5f4-39cd6ffe316d",
|
||||
"name": "Delete Me",
|
||||
"url": "otpauth:\/\/totp\/Facebook%3ADelete%20Me?digits=6&period=60&algorithm=SHA1&secret=GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV&issuer=Facebook"
|
||||
"issuer": "Scaleway",
|
||||
"info": {
|
||||
"secret": "MBQX47S3XOSOF7YT2CO3LZNACULZNY4P",
|
||||
"algo": "SHA256",
|
||||
"digits": 6,
|
||||
"period": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "a6276843-3c95-43b7-a3ef-e824c5534801",
|
||||
"name": "nodejsc0d3r",
|
||||
"url": "otpauth:\/\/totp\/Github%3Anodejsc0d3r?digits=6&period=50&algorithm=SHA1&secret=642GK5EROWKUAEBFRH6FWSMM4FRKS5IA&issuer=Github"
|
||||
},
|
||||
{
|
||||
"uuid": "159b3e5f-fff4-4ff5-997a-b8d1263ee446",
|
||||
"name": "MoneroMan",
|
||||
"url": "otpauth:\/\/totp\/Poloniex%3AMoneroMan?digits=8&period=30&algorithm=SHA256&secret=NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG&issuer=Poloniex"
|
||||
"issuer": "Github",
|
||||
"info": {
|
||||
"secret": "642GK5EROWKUAEBFRH6FWSMM4FRKS5IA",
|
||||
"algo": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hotp",
|
||||
"uuid": "1748b48c-7496-4722-9048-17248e843773",
|
||||
"name": "Lil' Droplet",
|
||||
"url": "otpauth:\/\/totp\/DigitalOcean%3ALil'%20Droplet?digits=6&period=45&algorithm=SHA512&secret=6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC&issuer=DigitalOcean"
|
||||
},
|
||||
{
|
||||
"uuid": "8cd843e5-4122-48e1-8089-a3d0c769741f",
|
||||
"name": "givemeabucket",
|
||||
"url": "otpauth:\/\/totp\/Bitbucket%3Agivemeabucket?digits=6&period=20&algorithm=SHA1&secret=5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY&issuer=Bitbucket"
|
||||
},
|
||||
{
|
||||
"uuid": "77c6267a-b38a-40b9-a3a7-5a5ce11230bb",
|
||||
"name": "Pepe",
|
||||
"url": "otpauth:\/\/totp\/4chan%3APepe?digits=8&period=30&algorithm=SHA1&secret=EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W&issuer=4chan"
|
||||
"issuer": "DigitalOcean",
|
||||
"info": {
|
||||
"secret": "6CAIGVYB5MQ6TSZLJ56HJBWU5S3H7FUC",
|
||||
"algo": "SHA512",
|
||||
"digits": 6,
|
||||
"counter": 96
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "9eef3fa3-bdde-4954-bf37-58bf1d5d6e5f",
|
||||
"name": "alex",
|
||||
"url": "otpauth:\/\/totp\/TTRSS%3Aalex?digits=6&period=30&algorithm=SHA1&secret=BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL&issuer=TTRSS"
|
||||
"issuer": "TTRSS",
|
||||
"info": {
|
||||
"secret": "BEBXDJJVXKX3ZMAZLJUU5I5PONI4IYAL",
|
||||
"algo": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "159b3e5f-fff4-4ff5-997a-b8d1263ee446",
|
||||
"name": "MoneroMan",
|
||||
"issuer": "Poloniex",
|
||||
"info": {
|
||||
"secret": "NUSLOF6CFEWNPAARBVJ5WFA5YRRQU4FG",
|
||||
"algo": "SHA256",
|
||||
"digits": 8,
|
||||
"period": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "8cd843e5-4122-48e1-8089-a3d0c769741f",
|
||||
"name": "givemeabucket",
|
||||
"issuer": "Bitbucket",
|
||||
"info": {
|
||||
"secret": "5KY3D2N53MRMIOMNKLLTNF3TDQJRDMEY",
|
||||
"algo": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "hotp",
|
||||
"uuid": "77c6267a-b38a-40b9-a3a7-5a5ce11230bb",
|
||||
"name": "Pepe",
|
||||
"issuer": "4chan",
|
||||
"info": {
|
||||
"secret": "EIQMT7NHFYJUMBKQ35P34JGLG3MO7L2W",
|
||||
"algo": "SHA1",
|
||||
"digits": 8,
|
||||
"counter": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "435a3542-c566-4cc0-a5f4-39cd6ffe316d",
|
||||
"name": "Delete Me",
|
||||
"issuer": "Facebook",
|
||||
"info": {
|
||||
"secret": "GSQ7E5FO3UFFVANS3NSMFE7RTMRMYNCV",
|
||||
"algo": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "totp",
|
||||
"uuid": "7dca76af-fa6a-4585-8d47-5a8b78130c9c",
|
||||
"name": "Oh wait, they don't have TOTP",
|
||||
"issuer": "Namecheap",
|
||||
"info": {
|
||||
"secret": "HIMV4HX3REXMUG236OOLN4GS7URI247Y",
|
||||
"algo": "SHA512",
|
||||
"digits": 8,
|
||||
"period": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue