Compare commits

...
34 Commits
Author SHA1 Message Date
Michael Schättgen ede9f93a59 Release v0.2.2 2019-03-28 13:46:39 +01:00
Michael Schättgen c6c0eb88e9 Fix true black theme in all activities
Closes #32. Also tested on API 19.
2019-03-28 13:46:33 +01:00
Michael Schättgen f929790be6 Fix metadata path by adding android folder
Closes #36
2019-03-28 13:46:07 +01:00
Michael Schättgen a4ff9d6d31 Fix crash in API 19 2019-03-28 13:45:43 +01:00
Michael SchättgenandGitHub a21d28a91f Update README.md 2019-03-28 10:47:24 +00:00
Michael Schättgen f7c32bc28b Improve theme based texts 2019-03-28 01:27:07 +01:00
Michael Schättgen 59c0ca947d Add workaround for amoled themed preferences 2019-03-28 01:08:44 +01:00
Michael Schättgen a7ac1adb80 Convert dark mode preference to select theme preference 2019-03-28 00:56:29 +01:00
Michael Schättgen 17204cc026 Add status bar color for true dark theme 2019-03-28 00:54:50 +01:00
Michael Schättgen cf4aecbd3e Refactor dark mode to allow multiple themes 2019-03-28 00:54:30 +01:00
Michael Schättgen 9baaf824c6 Start working on amoled / true black theme
#32
2019-03-28 00:15:02 +01:00
Michael Schättgen eb172fc54e Merge branch 'master' of https://github.com/alexbakker/Aegis 2019-03-27 21:18:26 +01:00
Michael Schättgen e99762b814 Change global progressbar color to improve visibility
As part of #34
2019-03-27 21:18:16 +01:00
Alexander Bakker 6b1d4a60e3 Credit Freepik from Flaticon for the icon 2019-03-27 12:32:40 +01:00
Alexander Bakker a540b3cfee Add icon to fastlane metadata 2019-03-27 12:14:08 +01:00
Michael SchättgenandGitHub b0481d6b30 Update README.md 2019-03-27 11:07:03 +00:00
Alexander Bakker 0c315c53e5 Add .idea/ and release/ to .gitignore 2019-03-27 01:03:55 +01:00
Alexander Bakker 75c37a454f Release v0.2.1 2019-03-27 00:51:44 +01:00
Michael Schättgen 554d7e17be Fix NullPointerException when deleting tokens 2019-03-27 00:47:39 +01:00
Alexander Bakker 52e4c5cd51 Release v0.2 2019-03-26 23:32:25 +01:00
Alexander Bakker 14b8ba89c8 Fix the last couple of issues with tap-to-reveal 2019-03-26 22:10:29 +01:00
Michael SchättgenandGitHub c9b312a398 Update README.md 2019-03-26 12:51:15 +00:00
Alexander Bakker 9fabd7fe10 Prepare fastlane structure for F-Droid 2019-03-26 09:03:13 +01:00
Alexander Bakker 7f1cea4202 Remove accidentally committed file 2019-03-26 01:03:19 +01:00
Alexander Bakker 29aa81765b Rewrite the HOTP/TOTP code to be more compact and support more digits 2019-03-26 00:59:57 +01:00
Alexander Bakker 066a7447c0 Support an arbitrary number of digits 2019-03-26 00:59:51 +01:00
Alexander Bakker 4cf1a5efe4 Acknowledge libsu in the README 2019-03-26 00:57:27 +01:00
Michael Schättgen a5778e6b38 Show/Hide action button depending on scroll direction 2019-03-26 00:53:32 +01:00
Michael Schättgen cebf15ba44 Add ability to adjust tap to reveal timeout 2019-03-26 00:06:39 +01:00
Michael Schättgen 16bf195ac0 Fix bug in codeIsRevealed when recycling entries 2019-03-25 21:49:50 +01:00
Michael Schättgen d57fc8b79a Add tap to reveal preference to results 2019-03-25 21:40:08 +01:00
Michael Schättgen 0e6828fae1 Add ability to tap to reveal codes 👀 2019-03-25 21:32:29 +01:00
Alexander Bakker ad1d3f04a6 Add support for importing from Google Authenticator
Close #29
2019-03-23 22:43:24 +01:00
Alexander Bakker b33c9383fe Update gradle 2019-03-23 18:16:32 +01:00
60 changed files with 903 additions and 614 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ out/
# Gradle files
.gradle/
build/
release/
# Local configuration file (sdk path, etc)
local.properties
@@ -34,8 +35,7 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
.idea/libraries
.idea/
# Keystore files
*.jks
Generated
-1
View File
@@ -1 +0,0 @@
Aegis
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>
-3
View File
@@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="F:\Android\Android Studio\gradle\gradle-2.14.1" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/Aegis.iml" filepath="$PROJECT_DIR$/Aegis.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+19 -8
View File
@@ -10,12 +10,16 @@ Aegis is a free, secure and open source 2FA app for Android.
- Password (scrypt)
- Fingerprint (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
- Scan QR code
- Enter details manually
- Import
- Import from files
- andOTP
- FreeOTP
- Aegis
- Import from apps (requires root):
- Google Authenticator
- Supported algorithms:
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
@@ -25,18 +29,20 @@ Aegis is a free, secure and open source 2FA app for Android.
- Drag and drop
- Custom groups
- Advanced entry editing
- Material design
- Light and dark themes
- Material design with multiple themes:
- Light theme
- Dark theme
- Amoled / true dark theme
- Export (plaintext or encrypted)
## Screenshots
[<img width=200 alt="Main Activity" src="assets/screenshot_main.png?raw=true">](assets/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="assets/screenshot_settings.png?raw=true">](/assets/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="assets/screenshot_edit.png?raw=true">](/assets/screenshot_edit.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_group.png?raw=true">](assets/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_dark.png?raw=true">](assets/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
## Downloads
@@ -56,7 +62,12 @@ There are plans to make it available on F-Droid as well.
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning Dodenhof
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
## License
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
## Attribution
The icon is largely based on a design by Freepik from Flaticon.
+5 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "0.1.1"
versionCode 5
versionName "0.2.2"
}
buildTypes {
debug {
@@ -29,6 +29,7 @@ android {
}
dependencies {
def libsuVersion = '2.3.2'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
@@ -45,5 +46,7 @@ dependencies {
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'me.dm7.barcodescanner:zxing:1.9'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
testImplementation 'junit:junit:4.12'
}
@@ -15,6 +15,10 @@ public class Preferences {
return _prefs.getBoolean("pref_dark_mode", false);
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSecureScreenEnabled() {
return _prefs.getBoolean("pref_secure_screen", true);
}
@@ -31,6 +35,24 @@ public class Preferences {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
public void setTapToRevealTime(int number) {
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
}
public int getTapToRevealTime() {
return _prefs.getInt("pref_tap_to_reveal_time", 30);
}
public int getCurrentTheme() {
return _prefs.getInt("pref_current_theme", 0);
}
public void setCurrentTheme(Theme theme) {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
@@ -0,0 +1,39 @@
package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED;
public static Theme fromInteger(int x) {
switch(x) {
case 0:
return LIGHT;
case 1:
return DARK;
case 2:
return AMOLED;
}
return null;
}
public static String getThemeName(int x) {
switch(x) {
case 0:
return "Light theme";
case 1:
return "Dark theme";
case 2:
return "Amoled theme";
}
return null;
}
public static String[] getThemeNames() {
return new String[] {
"Light theme",
"Dark theme",
"Amoled theme"
};
}
}
@@ -1,189 +1,47 @@
package com.beemdevelopment.aegis.crypto.otp;
/*
* OneTimePasswordAlgorithm.java
* OATH Initiative,
* HOTP one-time password algorithm
*
*/
/* Copyright (C) 2004, OATH. All rights reserved.
*
* License to copy and use this software is granted provided that it
* is identified as the "OATH HOTP Algorithm" in all material
* mentioning or referencing this software or this function.
*
* License is also granted to make and use derivative works provided
* that such works are identified as
* "derived from OATH HOTP algorithm"
* in all material mentioning or referencing the derived work.
*
* OATH (Open AuTHentication) and its members make no
* representations concerning either the merchantability of this
* software or the suitability of this software for any particular
* purpose.
*
* It is provided "as is" without express or implied warranty
* of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
* ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
*
* These notices must be retained in any copies of any part of this
* documentation and/or software.
*/
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class contains static methods that are used to calculate the
* One-Time Password (OTP) using
* JCE to provide the HMAC-SHA-1.
*
* @author Loren Hart
* @version 1.0
*/
public class HOTP {
private HOTP() {
}
// These are used to calculate the check-sum digits.
// 0 1 2 3 4 5 6 7 8 9
private static final int[] doubleDigits =
{0, 2, 4, 6, 8, 1, 3, 5, 7, 9};
/**
* Calculates the checksum using the credit card algorithm.
* This algorithm has the advantage that it detects any single
* mistyped digit and any single transposition of
* adjacent digits.
*
* @param num the number to calculate the checksum for
* @param digits number of significant places in the number
* @return the checksum of num
*/
public static int calcChecksum(long num, int digits) {
boolean doubleDigit = true;
int total = 0;
while (0 < digits--) {
int digit = (int) (num % 10);
num /= 10;
if (doubleDigit) {
digit = doubleDigits[digit];
}
total += digit;
doubleDigit = !doubleDigit;
}
int result = total % 10;
if (result > 0) {
result = 10 - result;
}
return result;
}
/**
* This method uses the JCE to provide the HMAC-SHA-1
* algorithm.
* HMAC computes a Hashed Message Authentication Code and
* in this case SHA1 is the hash algorithm used.
*
* @param keyBytes the bytes to use for the HMAC-SHA-1 key
* @param text the message or text to be authenticated.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not a valid HMAC-SHA-1 key.
*/
public static byte[] hmac_sha1(byte[] keyBytes, byte[] text)
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
// try {
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException nsae) {
hmacSha1 = Mac.getInstance("HMAC-SHA-1");
}
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmacSha1.init(macKey);
return hmacSha1.doFinal(text);
// } catch (GeneralSecurityException gse) {
// throw new UndeclaredThrowableException(gse);
// }
}
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
/**
* This method generates an OTP value for the given
* set of parameters.
*
* @param secret the shared secret
* @param movingFactor the counter, time, or other value that
* changes on a per use basis.
* @param codeDigits the number of digits in the OTP, not
* including the checksum, if any.
* @param addChecksum a flag that indicates if a checksum digit
* should be appended to the OTP.
* @param truncationOffset the offset into the MAC result to
* begin truncation. If this value is out of
* the range of 0 ... 15, then dynamic
* truncation will be used.
* Dynamic truncation is when the last 4
* bits of the last byte of the MAC are
* used to determine the start offset.
* @return A numeric String in base 10 that includes
* {@link codeDigits} digits plus the optional checksum
* digit if requested.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not
* a valid HMAC-SHA-1 key.
*/
static public String generateOTP(byte[] secret,
long movingFactor,
int codeDigits,
boolean addChecksum,
int truncationOffset)
throws NoSuchAlgorithmException, InvalidKeyException {
// put movingFactor value into text byte array
String result = null;
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = new byte[8];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] hash = mac.doFinal(counterBytes);
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
// put selected bytes into result int
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
if ((0 <= truncationOffset) &&
(truncationOffset < (hash.length - 4))) {
offset = truncationOffset;
}
int binary =
((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long bin = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long otp = bin % (long) Math.pow(10, digits);
int otp = binary % DIGITS_POWER[codeDigits];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(otp));
while (res.length() < digits) {
res.insert(0, "0");
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
return res.toString();
}
}
@@ -1,170 +1,21 @@
package com.beemdevelopment.aegis.crypto.otp;
/**
Copyright (c) 2011 IETF Trust and the persons identified as
authors of the code. All rights reserved.
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
Redistribution and use in source and binary forms, with or without
modification, is permitted pursuant to, and subject to the license
terms contained in, the Simplified BSD License set forth in Section
4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info).
*/
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
/**
* This is an example implementation of the OATH
* TOTP algorithm.
* Visit www.openauthentication.org for more information.
*
* @author Johan Rydell, PortWise, Inc.
*/
public class TOTP {
private TOTP() {
}
/**
* This method uses the JCE to provide the crypto algorithm.
* HMAC computes a Hashed Message Authentication Code with the
* crypto hash algorithm as a parameter.
*
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
* HmacSHA512)
* @param keyBytes: the bytes to use for the HMAC key
* @param text: the message or text to be authenticated
*/
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text) {
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
/**
* This method converts a HEX string to Byte[]
*
* @param hex: the HEX string
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex) {
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i + 1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP256(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP512(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @param crypto: the crypto function to use
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits,
String crypto) {
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16)
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] hash = hmac_sha(crypto, key, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[returnDigits];
result = Integer.toString(otp);
while (result.length() < returnDigits) {
result = "0" + result;
}
return result;
public static String generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
}
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONObject;
import java.util.List;
@@ -12,12 +14,12 @@ import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter {
public class AegisFileImporter extends DatabaseFileImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisImporter(ByteInputStream stream) {
super(stream);
public AegisFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
@@ -1,5 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -17,11 +19,11 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AndOtpImporter extends DatabaseImporter {
public class AndOtpFileImporter extends DatabaseFileImporter {
private JSONArray _obj;
public AndOtpImporter(ByteInputStream stream) {
super(stream);
public AndOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
@@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class DatabaseAppImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseAppImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseAppImporter>> importers = new LinkedHashMap<>();
importers.put("Google Authenticator", GoogleAuthAppImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected DatabaseAppImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseAppImporter create(Context context, Class<? extends DatabaseAppImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseAppImporter>> getImporters() {
return _importers;
}
}
@@ -0,0 +1,55 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
public abstract class DatabaseFileImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseFileImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseFileImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisFileImporter.class);
importers.put("andOTP", AndOtpFileImporter.class);
importers.put("FreeOTP", FreeOtpFileImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected ByteInputStream _stream;
protected DatabaseFileImporter(Context context, ByteInputStream stream) {
_context = context;
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class<? extends DatabaseFileImporter> type) {
try {
return type.getConstructor(Context.class, ByteInputStream.class).newInstance(context, stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseFileImporter>> getImporters() {
return _importers;
}
}
@@ -1,47 +1,14 @@
package com.beemdevelopment.aegis.importers;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
public abstract class DatabaseImporter {
private static Map<String, Class<? extends DatabaseImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisImporter.class);
importers.put("andOTP", AndOtpImporter.class);
importers.put("FreeOTP", FreeOtpImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
import java.util.List;
protected ByteInputStream _stream;
protected DatabaseImporter(ByteInputStream stream) {
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public static DatabaseImporter create(ByteInputStream stream, Class<? extends DatabaseImporter> type) {
try {
return type.getConstructor(ByteInputStream.class).newInstance(stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return _importers;
}
public interface DatabaseImporter {
void parse() throws DatabaseImporterException;
List<DatabaseEntry> convert() throws DatabaseImporterException;
boolean isEncrypted();
Context getContext();
}
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import org.json.JSONArray;
@@ -19,11 +20,11 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class FreeOtpImporter extends DatabaseImporter {
public class FreeOtpFileImporter extends DatabaseFileImporter {
private List<XmlEntry> _xmlEntries;
public FreeOtpImporter(ByteInputStream stream) {
super(stream);
public FreeOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
private static class XmlEntry {
@@ -0,0 +1,124 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthAppImporter extends DatabaseAppImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@SuppressLint("SdCardPath")
private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases";
private List<DatabaseEntry> _entries = new ArrayList<>();
public GoogleAuthAppImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (SuFileInputStream in = new SuFileInputStream(new SuFile(_filename))) {
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(in, out);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
if (!cursor.moveToFirst()) {
return;
}
do {
int type = getInt(cursor, "type");
byte[] secret = Base32.decode(getString(cursor, "secret").toCharArray());
OtpInfo info;
switch (type) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, getInt(cursor, "counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name = getString(cursor, "email", "");
String issuer = getString(cursor, "issuer", "");
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
_entries.add(entry);
} while(cursor.moveToNext());
}
} catch (SQLiteException | OtpInfoException | Base32Exception e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@Override
public List<DatabaseEntry> convert() {
return _entries;
}
@Override
public boolean isEncrypted() {
return false;
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
}
@@ -11,9 +11,13 @@ import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
private long _counter;
public HotpInfo(byte[] secret) throws OtpInfoException {
public HotpInfo(byte[] secret, long counter) throws OtpInfoException {
super(secret);
setCounter(0);
setCounter(counter);
}
public HotpInfo(byte[] secret) throws OtpInfoException {
this(secret, 0);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
@@ -24,7 +28,7 @@ public class HotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
return HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}
@@ -78,7 +78,8 @@ public abstract class OtpInfo implements Serializable {
}
public static boolean isDigitsValid(int digits) {
return digits == 6 || digits == 8;
// allow a max of 10 digits, as truncation will only extract 31 bits
return digits > 0 && digits <= 10;
}
public void setDigits(int digits) throws OtpInfoException {
@@ -5,6 +5,9 @@ import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
private int _period;
@@ -20,8 +23,11 @@ public class TotpInfo extends OtpInfo {
@Override
public String getOtp() {
String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod());
return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true));
try {
return TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
@@ -7,6 +7,7 @@ import android.view.WindowManager;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
public abstract class AegisActivity extends AppCompatActivity {
private AegisApplication _app;
@@ -22,7 +23,7 @@ public abstract class AegisActivity extends AppCompatActivity {
}
// set the theme
setPreferredTheme(getPreferences().isDarkModeEnabled());
setPreferredTheme(Theme.fromInteger(getPreferences().getCurrentTheme()));
}
protected AegisApplication getApp() {
@@ -33,11 +34,19 @@ public abstract class AegisActivity extends AppCompatActivity {
return _app.getPreferences();
}
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack);
break;
}
}
}
@@ -11,6 +11,7 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.TextView;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
@@ -152,6 +153,24 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
numberPicker.setMinValue(3);
numberPicker.setMaxValue(60);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
numberPicker.setWrapSelectorWheel(true);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onNumberInputResult(numberPicker.getValue()))
.create();
showSecureDialog(dialog);
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
@@ -196,6 +215,10 @@ public class Dialogs {
showSecureDialog(dialog);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
public interface TextInputListener {
void onTextInputResult(String text);
}
@@ -30,6 +30,7 @@ import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@@ -74,6 +75,7 @@ public class EditEntryActivity extends AegisActivity {
private EditText _textIssuer;
private EditText _textPeriod;
private EditText _textCounter;
private EditText _textDigits;
private EditText _textSecret;
private TableRow _rowPeriod;
@@ -81,7 +83,6 @@ public class EditEntryActivity extends AegisActivity {
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerDigits;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
@@ -116,6 +117,7 @@ public class EditEntryActivity extends AegisActivity {
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
_textDigits = findViewById(R.id.text_digits);
_rowPeriod = findViewById(R.id.row_period);
_textCounter = findViewById(R.id.text_counter);
_rowCounter = findViewById(R.id.row_counter);
@@ -124,8 +126,6 @@ public class EditEntryActivity extends AegisActivity {
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
@@ -158,6 +158,7 @@ public class EditEntryActivity extends AegisActivity {
} else {
throw new RuntimeException();
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
@@ -171,9 +172,6 @@ public class EditEntryActivity extends AegisActivity {
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String digits = Integer.toString(_origEntry.getInfo().getDigits());
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
@@ -256,15 +254,6 @@ public class EditEntryActivity extends AegisActivity {
}
}
@Override
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
}
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -439,7 +428,7 @@ public class EditEntryActivity extends AegisActivity {
int digits;
try {
digits = Integer.parseInt(_spinnerDigits.getSelectedItem().toString());
digits = Integer.parseInt(_textDigits.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Digits is not an integer.");
}
@@ -1,6 +1,8 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -12,6 +14,9 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.Toast;
@@ -31,6 +36,9 @@ import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseManager;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
private static final int CODE_SCAN = 0;
@@ -67,6 +75,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
@@ -158,7 +168,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.refresh(true);
}
}
@@ -355,8 +369,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteEntry(entry);
// update the filter list if the group no longer exists
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
if (entry.getGroup() != null) {
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
}
});
});
@@ -493,4 +509,39 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public void onEntryChange(DatabaseEntry entry) {
saveDatabase();
}
private boolean isAnimating;
@Override
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !isAnimating) {
isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
}
else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
}
}
@@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@@ -36,4 +39,21 @@ public class PreferencesActivity extends AegisActivity {
outState.putParcelable("result", _fragment.getResult());
super.onSaveInstanceState(outState);
}
@Override
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_Preferences);
break;
}
}
}
@@ -9,14 +9,22 @@ import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Toast;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.AegisFileImporter;
import com.beemdevelopment.aegis.importers.DatabaseAppImporter;
import com.beemdevelopment.aegis.importers.DatabaseFileImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
@@ -46,6 +54,7 @@ import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.topjohnwu.superuser.Shell;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
@@ -63,8 +72,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseImporter _importer;
private Class<? extends DatabaseImporter> _importerType;
private DatabaseFileImporter _importer;
private Class<? extends DatabaseFileImporter> _importerType;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
@@ -82,7 +91,33 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// set the result intent in advance
setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary("Selected: " + Theme.getThemeName(currentTheme));
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
String[] themeNames = Theme.getThemeNames();
int checkedTheme = app.getPreferences().getCurrentTheme();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_theme))
.setSingleChoiceItems(themeNames, checkedTheme, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setPositiveButton(android.R.string.ok, null)
.create());
return true;
}
});
darkModePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
@@ -92,8 +127,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference exportPreference = findPreference("pref_import");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
@@ -101,8 +136,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference importPreference = findPreference("pref_export");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
Preference importAppPreference = findPreference("pref_import_app");
importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImportApp();
return true;
}
});
Preference exportPreference = findPreference("pref_export");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onExport();
@@ -129,6 +174,12 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference screenPreference = findPreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@@ -144,6 +195,23 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
tapToRevealTimePreference.setSummary(app.getPreferences().getTapToRevealTime() + " seconds");
tapToRevealTimePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Dialogs.showNumberPickerDialog(getActivity(), new Dialogs.NumberInputListener() {
@Override
public void onNumberInputResult(int number) {
app.getPreferences().setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
_result.putExtra("needsRefresh", true);
}
});
return false;
}
});
_encryptionPreference = (SwitchPreference) findPreference("pref_encryption");
_encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@@ -277,7 +345,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
Map<String, Class<? extends DatabaseFileImporter>> importers = DatabaseFileImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
@@ -296,6 +364,43 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
.create());
}
private void onImportApp() {
Map<String, Class<? extends DatabaseAppImporter>> importers = DatabaseAppImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
try {
DatabaseAppImporter importer;
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importer = DatabaseAppImporter.create(getContext(), importers.get(names[i]));
importer.parse();
} catch (IOException e) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
})
.create());
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importer = null;
@@ -303,7 +408,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
((AegisImporter)_importer).setCredentials(creds);
((AegisFileImporter)_importer).setCredentials(creds);
try {
importDatabase(_importer);
@@ -337,15 +442,15 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
try {
DatabaseImporter importer = DatabaseImporter.create(stream, _importerType);
DatabaseFileImporter importer = DatabaseFileImporter.create(getContext(), stream, _importerType);
importer.parse();
// special case to decrypt encrypted aegis databases
if (importer.isEncrypted() && importer instanceof AegisImporter) {
if (importer.isEncrypted() && importer instanceof AegisFileImporter) {
_importer = importer;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter)_importer).getFile().getHeader().getSlots());
intent.putExtra("slots", ((AegisFileImporter)_importer).getFile().getHeader().getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
return;
}
@@ -8,6 +8,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.Theme;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
@@ -57,7 +58,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
}
@Override
protected void setPreferredTheme(boolean darkMode) {
protected void setPreferredTheme(Theme theme) {
setTheme(R.style.AppTheme_Fullscreen);
}
@@ -24,6 +24,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private boolean _showAccountName;
private boolean _tapToReveal;
private int _tapToRevealTime;
private String _groupFilter;
// keeps track of the viewholders that are currently bound
@@ -40,6 +42,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_showAccountName = showAccountName;
}
public void setTapToReveal(boolean tapToReveal) {
_tapToReveal = tapToReveal;
}
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (!isEntryFiltered(entry)) {
@@ -188,7 +198,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _shownEntries.get(position);
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress);
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
holder.setTapToRevealTime(_tapToRevealTime);
if (showProgress) {
holder.startRefreshLoop();
}
@@ -197,7 +208,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
_listener.onEntryClick(_shownEntries.get(position));
if (_tapToReveal && holder.isCodeHidden()) {
holder.revealCode();
} else {
_listener.onEntryClick(_shownEntries.get(position));
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@@ -212,7 +227,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void onClick(View v) {
// this will only be called if the entry is of type HotpInfo
try {
((HotpInfo)entry.getInfo()).incrementCounter();
((HotpInfo) entry.getInfo()).incrementCounter();
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
@@ -17,6 +17,7 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import android.os.Handler;
public class EntryHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
@@ -26,12 +27,20 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private View _currentView;
private boolean _hidden;
private int _tapToRevealTime;
private PeriodProgressBar _progressBar;
private UiRefresher _refresher;
private Handler _hiddenHandler;
public EntryHolder(final View view) {
super(view);
_currentView = view;
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
@@ -45,7 +54,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refreshCode();
if (!isCodeHidden()) {
refreshCode();
}
_progressBar.refresh();
}
@@ -54,10 +66,13 @@ public class EntryHolder extends RecyclerView.ViewHolder {
return ((TotpInfo)_entry.getInfo()).getMillisTillNextRotation();
}
});
_hiddenHandler = new Handler();
}
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress) {
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress, boolean hidden) {
_entry = entry;
_hidden = hidden;
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
@@ -83,7 +98,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileDrawable.setImageDrawable(drawable);
}
refreshCode();
// cancel any scheduled hideCode calls
_hiddenHandler.removeCallbacksAndMessages(null);
if (_hidden) {
hideCode();
} else {
refreshCode();
}
}
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
}
public void setOnRefreshClickListener(View.OnClickListener listener) {
@@ -99,7 +125,31 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void refreshCode() {
if (!isCodeHidden()) {
updateCode();
}
}
private void updateCode() {
String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
String text = otp.substring(0, (otp.length() / 2)
+ (otp.length() % 2)) + " "
+ otp.substring(otp.length() / 2);
_profileCode.setText(text);
}
public void revealCode() {
updateCode();
_hiddenHandler.postDelayed(this::hideCode, _tapToRevealTime * 1000);
_hidden = false;
}
private void hideCode() {
_profileCode.setText(_currentView.getContext().getResources().getString(R.string.tap_to_reveal));
_hidden = true;
}
public boolean isCodeHidden() {
return _hidden;
}
}
@@ -1,8 +1,9 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -33,6 +34,8 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
private UiRefresher _refresher;
private RecyclerView.OnScrollChangeListener _onScrollListener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -45,11 +48,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
// set up the recycler view
_rvKeyProfiles = view.findViewById(R.id.rvKeyProfiles);
_rvKeyProfiles.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_listener.onScroll(dx, dy);
}
});
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_rvKeyProfiles.setLayoutManager(mLayoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
@@ -151,6 +160,14 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.setShowAccountName(showAccountName);
}
public void setTapToReveal(boolean tapToReveal) {
_adapter.setTapToReveal(tapToReveal);
}
public void setTapToRevealTime(int number) {
_adapter.setTapToRevealTime(number);
}
public void addEntry(DatabaseEntry entry) {
_adapter.addEntry(entry);
checkPeriodUniformity();
@@ -191,5 +208,7 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onScroll(int dx, int dy);
}
}
@@ -0,0 +1,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="@color/colorAccent"/>
</shape>
</clip>
</item>
</layer-list>
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.AuthActivity">
<LinearLayout
android:orientation="vertical"
@@ -2,6 +2,8 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="?attr/background"
android:layout_height="match_parent">
<LinearLayout
@@ -171,9 +173,8 @@
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/spinner_type"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="4"
android:layout_marginLeft="-6dp"
android:paddingRight="6dp"
android:gravity="left"
android:layout_gravity="left"
@@ -182,23 +183,19 @@
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/spinner_algo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:layout_weight="4"
android:layout_marginLeft="-6dp"
android:paddingRight="6dp"
android:textAlignment="center"
android:backgroundTint="#949494"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/spinner_digits"
<EditText
android:id="@+id/text_digits"
android:hint="@string/digits"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="-6dp"
android:paddingRight="6dp"
android:layout_weight="3"
android:textAlignment="center"
android:backgroundTint="#949494"
style="@style/Base.Widget.AppCompat.Spinner.Underlined"/>
android:backgroundTint="#949494"/>
</LinearLayout>
</TableRow>
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.GroupManagerActivity">
<androidx.core.widget.NestedScrollView
@@ -5,6 +5,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.SlotManagerActivity">
@@ -2,6 +2,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="?attr/background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
@@ -11,7 +12,7 @@
android:id="@+id/copy_button"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:background="?attr/background"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
@@ -31,7 +32,7 @@
android:padding="16dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:background="?attr/background"
android:id="@+id/edit_button"
android:orientation="horizontal">
<ImageView
@@ -53,7 +54,7 @@
android:id="@+id/delete_button"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal">
<NumberPicker
android:id="@+id/numberPicker"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
@@ -10,6 +10,7 @@
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="4dp"
android:progressDrawable="@drawable/progress_horizontal"
android:id="@+id/progressBar"
android:visibility="gone"
android:max="1000"/>
+1
View File
@@ -35,6 +35,7 @@
<color name="hint_text_dark">#a7a7a7</color>
<color name="secondary_text_dark">#FF5252</color>
<color name="background_dark">#212121</color>
<color name="background_true_dark">#000000</color>
<color name="warning_color">#f4511e</color>
<color name="hint_color">#42000000</color>
+13 -9
View File
@@ -11,20 +11,24 @@
<string name="title_activity_intro">IntroActivity</string>
<string name="settings">Preferences</string>
<string name="pref_dark_mode_title">Dark mode</string>
<string name="pref_dark_mode_summary">Enable this to use darker colors</string>
<string name="pref_select_theme_title">Theme</string>
<string name="pref_account_name_title">Show the account name</string>
<string name="pref_account_name_summary">Enable this to show the account name next to the issuer</string>
<string name="pref_timeout_title">Timeout</string>
<string name="pref_timeout_summary">Automatically lock the database after %1$s seconds of inactivity</string>
<string name="pref_slots_title">Key slots</string>
<string name="pref_slots_summary">Manage the list of keys that can decrypt the database</string>
<string name="pref_import_title">Import</string>
<string name="pref_import_summary">Import a database</string>
<string name="pref_import_file_title">Import from file</string>
<string name="pref_import_file_summary">Import a database from a file</string>
<string name="pref_import_app_title">Import from app</string>
<string name="pref_import_app_summary">Import a database from an app (requires root access)</string>
<string name="pref_export_title">Export</string>
<string name="pref_export_summary">Export the database</string>
<string name="pref_secure_screen_title">Screen security</string>
<string name="pref_secure_screen_summary">Block screenshots and other attempts to capture the screen within the app</string>
<string name="pref_tap_to_reveal_title">Tap to reveal</string>
<string name="pref_tap_to_reveal_summary">Tokens will be hidden by default. Tap on the tokens to reveal code.</string>
<string name="pref_tap_to_reveal_time_title">Timeout for tap to reveal</string>
<string name="pref_encryption_title">Encryption</string>
<string name="pref_encryption_summary">Encrypt the database and unlock it with a password or fingerprint</string>
<string name="pref_fingerprint_title">Fingerprint</string>
@@ -48,6 +52,7 @@
<string name="authentication">Unlock the vault</string>
<string name="set_password">Please enter a password</string>
<string name="set_group">Please enter a group name</string>
<string name="set_number">Please enter a number</string>
<string name="set_password_confirm">Please confirm the password</string>
<string name="invalidated_fingerprint">A change in your device\'s security settings has been detected. Please go to \"Settings -> Fingerprint\" and re-add your fingerprint.</string>
@@ -68,17 +73,13 @@
<item>SHA512</item>
</string-array>
<string-array name="otp_digits_array">
<item>6</item>
<item>8</item>
</string-array>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="unlock">Unlock</string>
<string name="advanced">Advanced</string>
<string name="seconds">seconds</string>
<string name="counter">Counter</string>
<string name="digits">Digits</string>
<string name="secret">Secret</string>
<string name="scan">Scan QR code</string>
<string name="enter_manually">Enter manually</string>
@@ -118,9 +119,11 @@
<string name="encrypting_error">An error occurred while enabling encryption</string>
<string name="permission_denied">Permission denied</string>
<string name="choose_application">Select the application you\'d like to import a database from</string>
<string name="choose_theme">Select your desired theme</string>
<string name="parsing_file_error">An error occurred while trying to parse the file</string>
<string name="file_not_found">Error: File not found</string>
<string name="reading_file_error">An error occurred while trying to read the file</string>
<string name="root_error">Error: unable to obtain root access</string>
<string name="imported_entries_count">Imported %d entries</string>
<string name="exporting_database_error">An error occurred while trying to export the database</string>
<string name="export_database_location">The database has been exported to:</string>
@@ -147,4 +150,5 @@
<string name="group_name_hint">Group name</string>
<string name="preference_manage_groups">Edit groups</string>
<string name="preference_manage_groups_summary">Manage and delete your groups here</string>
<string name="tap_to_reveal">Hidden</string>
</resources>
+14
View File
@@ -66,6 +66,20 @@
<item name="swirl_ridgeColor">@color/colorSwirlPrimaryDark</item>
<item name="swirl_errorColor">@color/colorSwirlErrorDark</item>
</style>
<style name="AppTheme.TrueBlack" parent="AppTheme.Dark">
<item name="colorPrimary">@color/background_true_dark</item>
<item name="cardBackground">@color/background_true_dark</item>
<item name="background">@color/background_true_dark</item>
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
<item name="colorPrimaryDark">@color/background_true_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="AppTheme.TrueBlack.Preferences" parent="AppTheme.TrueBlack">
<item name="android:windowBackground">@color/background_true_dark</item>
</style>
<style name="AppTheme.Dark.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
+22 -5
View File
@@ -7,11 +7,10 @@
<PreferenceCategory
android:title="Appearance"
app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat
<Preference
android:defaultValue="false"
android:key="pref_dark_mode"
android:title="@string/pref_dark_mode_title"
android:summary="@string/pref_dark_mode_summary"
android:title="@string/pref_select_theme_title"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
@@ -37,6 +36,19 @@
android:title="@string/pref_secure_screen_title"
android:summary="@string/pref_secure_screen_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_tap_to_reveal"
android:title="@string/pref_tap_to_reveal_title"
android:summary="@string/pref_tap_to_reveal_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_tap_to_reveal_time"
android:title="@string/pref_tap_to_reveal_time_title"
android:dependency="pref_tap_to_reveal"
app:iconSpaceReserved="false"/>
<!--<EditTextPreference
android:key="pref_timeout"
android:title="@string/pref_timeout_title"
@@ -78,8 +90,13 @@
app:iconSpaceReserved="false">
<Preference
android:key="pref_import"
android:title="@string/pref_import_title"
android:summary="@string/pref_import_summary"
android:title="@string/pref_import_file_title"
android:summary="@string/pref_import_file_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_import_app"
android:title="@string/pref_import_app_title"
android:summary="@string/pref_import_app_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_export"
@@ -12,22 +12,22 @@ import static org.junit.Assert.*;
public class HOTPTest {
// https://tools.ietf.org/html/rfc4226#page-32
private final String[] _vectors = {
"755224", "287082",
"359152", "969429",
"338314", "254676",
"287922", "162583",
"399871", "520489"
"755224", "287082",
"359152", "969429",
"338314", "254676",
"287922", "162583",
"399871", "520489"
};
private final byte[] _secret = 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[] _secret = new byte[]{
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30
};
@Test
public void vectorsMatch() throws InvalidKeyException, NoSuchAlgorithmException {
for (int i = 0; i < _vectors.length; i++) {
String otp = HOTP.generateOTP(_secret, i, 6, false, -1);
String otp = HOTP.generateOTP(_secret, "HmacSHA1", 6, i);
assertEquals(_vectors[i], otp);
}
}
@@ -3,31 +3,49 @@ package com.beemdevelopment.aegis;
import org.junit.Test;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import static org.junit.Assert.*;
public class TOTPTest {
private static class Vector {
public long Time;
public String OTP;
public String Algo;
public Vector(long time, String otp, String algo) {
Time = time;
OTP = otp;
Algo = algo;
}
}
// https://tools.ietf.org/html/rfc6238#appendix-B
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 Vector[] _vectors = {
new Vector(59, "94287082", "HmacSHA1"),
new Vector(59, "46119246", "HmacSHA256"),
new Vector(59, "90693936", "HmacSHA512"),
new Vector(1111111109, "07081804", "HmacSHA1"),
new Vector(1111111109, "68084774", "HmacSHA256"),
new Vector(1111111109, "25091201", "HmacSHA512"),
new Vector(1111111111, "14050471", "HmacSHA1"),
new Vector(1111111111, "67062674", "HmacSHA256"),
new Vector(1111111111, "99943326", "HmacSHA512"),
new Vector(1234567890, "89005924", "HmacSHA1"),
new Vector(1234567890, "91819424", "HmacSHA256"),
new Vector(1234567890, "93441116", "HmacSHA512"),
new Vector(2000000000, "69279037", "HmacSHA1"),
new Vector(2000000000, "90698825", "HmacSHA256"),
new Vector(2000000000, "38618901", "HmacSHA512"),
new Vector(20000000000L, "65353130", "HmacSHA1"),
new Vector(20000000000L, "77737706", "HmacSHA256"),
new Vector(20000000000L, "47863826", "HmacSHA512")
};
private final byte[] _seed = new byte[]{
@@ -50,11 +68,11 @@ public class TOTPTest {
};
@Test
public void vectorsMatch() {
for (String[] vector : _vectors) {
public void vectorsMatch() throws NoSuchAlgorithmException, InvalidKeyException, HexException {
for (Vector vector : _vectors) {
byte[] seed;
switch (vector[2]) {
switch (vector.Algo) {
case "HmacSHA1":
seed = _seed;
break;
@@ -69,8 +87,8 @@ public class TOTPTest {
return;
}
String otp = TOTP.generateTOTP(seed, vector[0], 8, vector[2]);
assertEquals(vector[1], otp);
String otp = TOTP.generateOTP(seed, vector.Algo, 8, 30, vector.Time);
assertEquals(vector.OTP, otp);
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.android.tools.build:gradle:3.3.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+39
View File
@@ -0,0 +1,39 @@
Aegis Authenticator is a free, secure and open source app to manage your
2-step verification tokens for your online services.
'''Encryption'''
All of your one-time passwords are stored in a vault. If you choose
to set a password, which is highly recommended, the vault will be encrypted
using AES-256. If someone with malicious intent gets a hold of the vault file,
its impossible for them to retrieve the contents without knowing the password.
'''Fingerprint unlock'''
Entering your password each time you need access to a
one-time password can be cumbersome. Fortunately, you can also enable
fingerprint unlock if your device has a fingerprint scanner.
'''Compatibility'''
Aegis supports the HOTP and TOTP algorithms. These two algorithms are
industry-standard and widely supported, making Aegis compatible with
thousands of services. Some examples are: Google, GitHub, Dropbox, Facebook
and Instagram.
It is also compatible with Google Authenticator. Any website that shows a QR
code for Google Authenticator also works with Aegis.
'''Groups'''
Have a lot of one-time passwords? Add them to custom groups for easier
access. Personal, Work and Social can each get their own group.
'''Backups'''
To make sure you will never lose access to your online accounts Aegis
Authenticator supports exporting your vault which you can import onto a new
device. Aegis Authenticator also allows you to import AndOTP and FreeOTP
databases so switching to Aegis is made easier for you.
'''Open source and license'''
Aegis Authenticator is open source (licensed under GPL v3) and the source code
can be found here: http://github.com/beemdevelopment/Aegis
'''Attribution'''
The icon is largely based on a design by Freepik from Flaticon.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

+1
View File
@@ -0,0 +1 @@
Free, secure and open source 2FA app to manage tokens for your online services
+1
View File
@@ -0,0 +1 @@
Aegis Authenticator