Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede9f93a59 | ||
|
|
c6c0eb88e9 | ||
|
|
f929790be6 | ||
|
|
a4ff9d6d31 | ||
|
|
a21d28a91f | ||
|
|
f7c32bc28b | ||
|
|
59c0ca947d | ||
|
|
a7ac1adb80 | ||
|
|
17204cc026 | ||
|
|
cf4aecbd3e | ||
|
|
9baaf824c6 | ||
|
|
eb172fc54e | ||
|
|
e99762b814 | ||
|
|
6b1d4a60e3 | ||
|
|
a540b3cfee | ||
|
|
b0481d6b30 | ||
|
|
0c315c53e5 | ||
|
|
75c37a454f | ||
|
|
554d7e17be | ||
|
|
52e4c5cd51 | ||
|
|
14b8ba89c8 | ||
|
|
c9b312a398 | ||
|
|
9fabd7fe10 | ||
|
|
7f1cea4202 | ||
|
|
29aa81765b | ||
|
|
066a7447c0 | ||
|
|
4cf1a5efe4 | ||
|
|
a5778e6b38 | ||
|
|
cebf15ba44 | ||
|
|
16bf195ac0 | ||
|
|
d57fc8b79a | ||
|
|
0e6828fae1 | ||
|
|
ad1d3f04a6 | ||
|
|
b33c9383fe |
@@ -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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Aegis
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
it’s 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.
|
||||
|
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 |
@@ -0,0 +1 @@
|
||||
Free, secure and open source 2FA app to manage tokens for your online services
|
||||
@@ -0,0 +1 @@
|
||||
Aegis Authenticator
|
||||