Compare commits

..
61 Commits
Author SHA1 Message Date
Alexander Bakker f27dc54ec4 Release v0.3 2019-04-01 23:50:07 +02:00
Alexander Bakker 4c1dec613c Fallback to a 30 second period if an andOTP doesn't provide one 2019-04-01 23:48:44 +02:00
Alexander Bakker a251685002 Add a dialog that displays a list of errors that occurred during import 2019-04-01 23:36:31 +02:00
Alexander Bakker 118221e3df Add support for Steam tokens to the andOTP importer
Close #44
2019-04-01 22:37:56 +02:00
Alexander Bakker 1dd5f893da Add support for Steam OTP 2019-04-01 22:37:52 +02:00
Michael Schättgen a46640f43d Fix recreate when setting view mode 2019-04-01 21:47:41 +02:00
Michael Schättgen a293078603 Add way to set view mode 2019-04-01 01:44:38 +02:00
Michael Schättgen f3ed79dc71 Add ability to change view mode 2019-04-01 01:21:12 +02:00
Michael Schättgen ee8fd2e9f2 Hide divider in compact mode when progressbar is visible 2019-04-01 01:04:26 +02:00
Michael Schättgen 8b9ec5f976 Start working on compact mode 2019-04-01 00:58:45 +02:00
Michael Schättgen d283eae7a0 Add russian strings for authentication methods
#43
2019-04-01 00:18:44 +02:00
Michael SchättgenandGitHub a854db760c Merge pull request #41 from ferhadnecef/stringlate-ru-1048
Updated ru (Russian) translation
2019-03-31 23:57:36 +02:00
Michael Schättgen caf2ec882a Merge branch 'feature-sorting' 2019-03-31 23:30:52 +02:00
Michael Schättgen bbf967e2b7 Add a way to save current sorting method 2019-03-31 22:34:25 +02:00
Michael Schättgen b9d011b48e Disable drag and drop when list is sorted 2019-03-31 21:28:37 +02:00
Michael Schättgen ecdadf5335 Add SortCategory fixes and support for custom sorting 2019-03-31 21:23:14 +02:00
Michael Schättgen 10b80442d0 Comparators will now ignore casing 2019-03-31 21:22:20 +02:00
Alexander Bakker a2879a4558 Fix the screenshot links in the README 2019-03-31 20:39:41 +02:00
Michael Schättgen 62025d1053 Add way to check for reversed sort categories 2019-03-31 20:14:11 +02:00
Michael Schättgen 09fe550a28 Add AccountNameComparator 2019-03-31 20:12:34 +02:00
Michael Schättgen 07aac1e6a3 Add support for reversed sorting methods 2019-03-31 20:12:25 +02:00
Michael Schättgen b8939b504e Start working on entry sorting 2019-03-31 19:57:45 +02:00
Alexander Bakker 4f29d0c2e4 Fix metadata files for F-Droid (for real this time) 2019-03-31 18:21:10 +02:00
Alexander Bakker 592c6683c3 Continue importing entries even if one can't be parsed 2019-03-30 18:26:16 +01:00
ferhad.necef f8edac72ea Updated ru (Russian) translation 2019-03-29 16:00:49 +03:00
Alexander Bakker fc0e1150f6 Replace hardcoded encoding names with constants 2019-03-28 20:01:00 +01:00
Michael SchättgenandGitHub e1fd913817 Update README.md 2019-03-28 13:15:18 +00:00
Michael Schättgen ede9f93a59 Release v0.2.2 2019-03-28 13:46:39 +01:00
Michael Schättgen c6c0eb88e9 Fix true black theme in all activities
Closes #32. Also tested on API 19.
2019-03-28 13:46:33 +01:00
Michael Schättgen f929790be6 Fix metadata path by adding android folder
Closes #36
2019-03-28 13:46:07 +01:00
Michael Schättgen a4ff9d6d31 Fix crash in API 19 2019-03-28 13:45:43 +01:00
Michael SchättgenandGitHub a21d28a91f Update README.md 2019-03-28 10:47:24 +00:00
Michael Schättgen f7c32bc28b Improve theme based texts 2019-03-28 01:27:07 +01:00
Michael Schättgen 59c0ca947d Add workaround for amoled themed preferences 2019-03-28 01:08:44 +01:00
Michael Schättgen a7ac1adb80 Convert dark mode preference to select theme preference 2019-03-28 00:56:29 +01:00
Michael Schättgen 17204cc026 Add status bar color for true dark theme 2019-03-28 00:54:50 +01:00
Michael Schättgen cf4aecbd3e Refactor dark mode to allow multiple themes 2019-03-28 00:54:30 +01:00
Michael Schättgen 9baaf824c6 Start working on amoled / true black theme
#32
2019-03-28 00:15:02 +01:00
Michael Schättgen eb172fc54e Merge branch 'master' of https://github.com/alexbakker/Aegis 2019-03-27 21:18:26 +01:00
Michael Schättgen e99762b814 Change global progressbar color to improve visibility
As part of #34
2019-03-27 21:18:16 +01:00
Alexander Bakker 6b1d4a60e3 Credit Freepik from Flaticon for the icon 2019-03-27 12:32:40 +01:00
Alexander Bakker a540b3cfee Add icon to fastlane metadata 2019-03-27 12:14:08 +01:00
Michael SchättgenandGitHub b0481d6b30 Update README.md 2019-03-27 11:07:03 +00:00
Alexander Bakker 0c315c53e5 Add .idea/ and release/ to .gitignore 2019-03-27 01:03:55 +01:00
Alexander Bakker 75c37a454f Release v0.2.1 2019-03-27 00:51:44 +01:00
Michael Schättgen 554d7e17be Fix NullPointerException when deleting tokens 2019-03-27 00:47:39 +01:00
Alexander Bakker 52e4c5cd51 Release v0.2 2019-03-26 23:32:25 +01:00
Alexander Bakker 14b8ba89c8 Fix the last couple of issues with tap-to-reveal 2019-03-26 22:10:29 +01:00
Michael SchättgenandGitHub c9b312a398 Update README.md 2019-03-26 12:51:15 +00:00
Alexander Bakker 9fabd7fe10 Prepare fastlane structure for F-Droid 2019-03-26 09:03:13 +01:00
Alexander Bakker 7f1cea4202 Remove accidentally committed file 2019-03-26 01:03:19 +01:00
Alexander Bakker 29aa81765b Rewrite the HOTP/TOTP code to be more compact and support more digits 2019-03-26 00:59:57 +01:00
Alexander Bakker 066a7447c0 Support an arbitrary number of digits 2019-03-26 00:59:51 +01:00
Alexander Bakker 4cf1a5efe4 Acknowledge libsu in the README 2019-03-26 00:57:27 +01:00
Michael Schättgen a5778e6b38 Show/Hide action button depending on scroll direction 2019-03-26 00:53:32 +01:00
Michael Schättgen cebf15ba44 Add ability to adjust tap to reveal timeout 2019-03-26 00:06:39 +01:00
Michael Schättgen 16bf195ac0 Fix bug in codeIsRevealed when recycling entries 2019-03-25 21:49:50 +01:00
Michael Schättgen d57fc8b79a Add tap to reveal preference to results 2019-03-25 21:40:08 +01:00
Michael Schättgen 0e6828fae1 Add ability to tap to reveal codes 👀 2019-03-25 21:32:29 +01:00
Alexander Bakker ad1d3f04a6 Add support for importing from Google Authenticator
Close #29
2019-03-23 22:43:24 +01:00
Alexander Bakker b33c9383fe Update gradle 2019-03-23 18:16:32 +01:00
82 changed files with 2096 additions and 835 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ out/
# Gradle files
.gradle/
build/
release/
# Local configuration file (sdk path, etc)
local.properties
@@ -34,8 +35,7 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
.idea/libraries
.idea/
# Keystore files
*.jks
Generated
-1
View File
@@ -1 +0,0 @@
Aegis
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>
-3
View File
@@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="F:\Android\Android Studio\gradle\gradle-2.14.1" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/Aegis.iml" filepath="$PROJECT_DIR$/Aegis.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+19 -8
View File
@@ -10,12 +10,16 @@ Aegis is a free, secure and open source 2FA app for Android.
- Password (scrypt)
- Fingerprint (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
- Scan QR code
- Enter details manually
- Import
- Import from files
- andOTP
- FreeOTP
- Aegis
- Import from apps (requires root):
- Google Authenticator
- Supported algorithms:
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
@@ -25,18 +29,20 @@ Aegis is a free, secure and open source 2FA app for Android.
- Drag and drop
- Custom groups
- Advanced entry editing
- Material design
- Light and dark themes
- Material design with multiple themes:
- Light theme
- Dark theme
- Amoled / true dark theme
- Export (plaintext or encrypted)
## Screenshots
[<img width=200 alt="Main Activity" src="assets/screenshot_main.png?raw=true">](assets/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="assets/screenshot_settings.png?raw=true">](/assets/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="assets/screenshot_edit.png?raw=true">](/assets/screenshot_edit.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_group.png?raw=true">](assets/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_dark.png?raw=true">](assets/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
## Downloads
@@ -56,7 +62,12 @@ There are plans to make it available on F-Droid as well.
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning Dodenhof
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
## License
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
## Attribution
The icon is largely based on a design by Freepik from Flaticon.
+5 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "0.1.1"
versionCode 6
versionName "0.3"
}
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,43 @@ 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 void setCurrentSortCategory(SortCategory category) {
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
}
public int getCurrentSortCategory() {
return _prefs.getInt("pref_current_sort_category", 0);
}
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 getCurrentViewMode() {
return _prefs.getInt("pref_current_view_mode", 0);
}
public void setCurrentViewMode(ViewMode viewMode) {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
@@ -0,0 +1,73 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import java.util.Comparator;
public enum SortCategory {
CUSTOM,
ACCOUNT,
ACCOUNTREVERSED,
ISSUER,
ISSUERREVERSED;
public static SortCategory fromInteger(int x) {
switch(x) {
case 0:
return CUSTOM;
case 1:
return ACCOUNT;
case 2:
return ACCOUNTREVERSED;
case 3:
return ISSUER;
case 4:
return ISSUERREVERSED;
}
return null;
}
public static Comparator getComparator(SortCategory sortCategory) {
switch(sortCategory) {
case ACCOUNT:
case ACCOUNTREVERSED:
return new AccountNameComparator();
case ISSUER:
case ISSUERREVERSED:
return new IssuerNameComparator();
case CUSTOM:
return new IssuerNameComparator();
}
return null;
}
public static boolean isReversed(SortCategory sortCategory) {
switch(sortCategory) {
case ACCOUNTREVERSED:
case ISSUERREVERSED:
return true;
default:
return false;
}
}
public static int getMenuItem(SortCategory sortCategory) {
switch (sortCategory) {
case CUSTOM:
return R.id.menu_sort_custom;
case ACCOUNT:
return R.id.menu_sort_alphabetically_name;
case ACCOUNTREVERSED:
return R.id.menu_sort_alphabetically_name_reverse;
case ISSUER:
return R.id.menu_sort_alphabetically;
case ISSUERREVERSED:
return R.id.menu_sort_alphabetically_reverse;
default:
return R.id.menu_sort_custom;
}
}
}
@@ -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"
};
}
}
@@ -0,0 +1,33 @@
package com.beemdevelopment.aegis;
public enum ViewMode {
NORMAL,
COMPACT;
public static ViewMode fromInteger(int x) {
switch(x) {
case 0:
return NORMAL;
case 1:
return COMPACT;
}
return null;
}
public static String getViewModeName(int x) {
switch(x) {
case 0:
return "Normal";
case 1:
return "Compact";
}
return null;
}
public static String[] getViewModeNames() {
return new String[] {
"Normal",
"Compact"
};
}
}
@@ -6,7 +6,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -122,7 +122,7 @@ public class CryptoUtils {
private static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = Charset.forName("UTF-8").encode(charBuf);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
}
}
@@ -1,189 +1,40 @@
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 OTP 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);
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
return new OTP(otp, digits);
}
}
@@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private int _code;
private int _digits;
public OTP(int code, int digits) {
_code = code;
_digits = digits;
}
public int getCode() {
return _code;
}
public int getDigits() {
return _digits;
}
@NonNull
@Override
public String toString() {
int code = _code % (int) Math.pow(10, _digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(code));
while (res.length() < _digits) {
res.insert(0, "0");
}
return res.toString();
}
public String toSteamString() {
int code = _code;
StringBuilder res = new StringBuilder();
for (int i = 0; i < _digits; i++) {
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
res.append(c);
code /= STEAM_ALPHABET.length();
}
return res.toString();
}
}
@@ -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 OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
/**
* 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 OTP generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
}
@@ -12,7 +12,7 @@ import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
public class DatabaseFile {
public static final byte VERSION = 1;
@@ -54,8 +54,8 @@ public class DatabaseFile {
try {
String string = obj.toString(4);
return string.getBytes("UTF-8");
} catch (JSONException | UnsupportedEncodingException e) {
return string.getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@@ -79,9 +79,9 @@ public class DatabaseFile {
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
try {
JSONObject obj = new JSONObject(new String(data, "UTF-8"));
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return DatabaseFile.fromJson(obj);
} catch (UnsupportedEncodingException | JSONException e) {
} catch (JSONException e) {
throw new DatabaseFileException(e);
}
}
@@ -94,8 +94,8 @@ public class DatabaseFile {
try {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), "UTF-8"));
} catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
} catch (MasterKeyException | JSONException | Base64Exception e) {
throw new DatabaseFileException(e);
}
}
@@ -108,12 +108,12 @@ public class DatabaseFile {
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
try {
String string = obj.toString(4);
byte[] dbBytes = string.getBytes("UTF-8");
byte[] dbBytes = string.getBytes(StandardCharsets.UTF_8);
CryptResult result = creds.encrypt(dbBytes);
_content = Base64.encode(result.getData());
_header = new Header(creds.getSlots(), result.getParams());
} catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
} catch (MasterKeyException | JSONException e) {
throw new DatabaseFileException(e);
}
}
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis.encoding;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
@@ -19,11 +19,6 @@ public class Base64 {
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
try {
return new String(encoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return new String(encoded, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<DatabaseEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
@@ -0,0 +1,13 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.views.EntryHolder;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<DatabaseEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
@@ -0,0 +1,84 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisFileImporter extends DatabaseFileImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return DatabaseEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -1,62 +0,0 @@
package com.beemdevelopment.aegis.importers;
import org.json.JSONObject;
import java.util.List;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseException;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisImporter extends DatabaseImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisImporter(ByteInputStream stream) {
super(stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
Database db = Database.fromJson(obj);
return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -0,0 +1,99 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
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.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AndOtpFileImporter extends DatabaseFileImporter {
private JSONArray _obj;
public AndOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), StandardCharsets.UTF_8));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -1,85 +0,0 @@
package com.beemdevelopment.aegis.importers;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AndOtpImporter extends DatabaseImporter {
private JSONArray _obj;
public AndOtpImporter(ByteInputStream stream) {
super(stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = "";
String name = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
} catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -0,0 +1,51 @@
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.put("Steam", SteamAppImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected DatabaseAppImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
public abstract DatabaseImporterResult 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 DatabaseImporterResult 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;
DatabaseImporterResult convert() throws DatabaseImporterException;
boolean isEncrypted();
Context getContext();
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
}
public String getText() {
return _text;
}
}
@@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.importers;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.util.ArrayList;
import java.util.List;
public class DatabaseImporterResult {
private List<DatabaseEntry> _entries = new ArrayList<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public List<DatabaseEntry> getEntries() {
return _entries;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}
@@ -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 {
@@ -45,43 +46,48 @@ public class FreeOtpImporter extends DatabaseImporter {
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
try {
for (XmlEntry xmlEntry : _xmlEntries) {
if (xmlEntry.Name.equals("tokenOrder")) {
// TODO: order
JSONArray array = new JSONArray(xmlEntry.Value);
} else {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
for (XmlEntry xmlEntry : _xmlEntries) {
// TODO: order
if (!xmlEntry.Name.equals("tokenOrder")) {
try {
DatabaseEntry entry = convertEntry(xmlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
return result;
}
private static DatabaseEntry convertEntry(XmlEntry xmlEntry) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, xmlEntry.Value);
}
}
@Override
@@ -0,0 +1,180 @@
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<Entry> _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 {
Entry entry = new Entry(cursor);
_entries.add(entry);
} while(cursor.moveToNext());
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
for (Entry sqlEntry : _entries) {
try {
DatabaseEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new DatabaseEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
@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));
}
private static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
private static class Entry {
private int _type;
private String _secret;
private String _email;
private String _issuer;
private long _counter;
public Entry(Cursor cursor) {
_type = getInt(cursor, "type");
_secret = getString(cursor, "secret");
_email = getString(cursor, "email", "");
_issuer = getString(cursor, "issuer", "");
_counter = getLong(cursor, "counter");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getEmail() {
return _email;
}
public String getIssuer() {
return _issuer;
}
public long getCounter() {
return _counter;
}
}
}
@@ -0,0 +1,80 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class SteamAppImporter extends DatabaseAppImporter {
@SuppressLint("SdCardPath")
private static final String _path = "/data/data/com.valvesoftware.android.steam.community/files";
private List<JSONObject> _objs = new ArrayList<>();
public SteamAppImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
SuFile dir = new SuFile(_path);
for (SuFile file : dir.listFiles((d, name) -> name.startsWith("Steamguard-"))) {
try (SuFileInputStream in = new SuFileInputStream(file)) {
try (ByteInputStream stream = ByteInputStream.create(in)) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
_objs.add(obj);
}
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
for (JSONObject obj : _objs) {
try {
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
public DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException{
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new DatabaseEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import org.json.JSONException;
import org.json.JSONObject;
@@ -9,11 +10,17 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
public static final String ID = "hotp";
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 +31,8 @@ public class HotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}
@@ -32,7 +40,7 @@ public class HotpInfo extends OtpInfo {
@Override
public String getType() {
return "hotp";
return ID;
}
@Override
@@ -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 {
@@ -97,13 +98,14 @@ public abstract class OtpInfo implements Serializable {
int digits = obj.getInt("digits");
switch (type) {
case "totp":
int period = obj.getInt("period");
info = new TotpInfo(secret, algo, digits, period);
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "hotp":
long counter = obj.getLong("counter");
info = new HotpInfo(secret, algo, digits, counter);
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, obj.getInt("period"));
break;
case HotpInfo.ID:
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new OtpInfoException("unsupported otp type: " + type);
@@ -125,7 +127,8 @@ public abstract class OtpInfo implements Serializable {
}
OtpInfo info = (OtpInfo) o;
return Arrays.equals(getSecret(), info.getSecret())
return getType().equals(info.getType())
&& Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
}
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
public SteamInfo(byte[] secret) throws OtpInfoException {
super(secret, "SHA1", 5, 30);
}
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
super(secret, algorithm, digits, period);
}
@Override
public String getOtp() {
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toSteamString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return ID;
}
}
@@ -1,11 +1,17 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
public static final String ID = "totp";
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
@@ -20,13 +26,17 @@ 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 {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return "totp";
return ID;
}
@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);
}
@@ -38,6 +38,7 @@ import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import java.io.ByteArrayInputStream;
@@ -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) {
@@ -166,14 +167,11 @@ public class EditEntryActivity extends AegisActivity {
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String digits = Integer.toString(_origEntry.getInfo().getDigits());
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
@@ -192,11 +190,12 @@ public class EditEntryActivity extends AegisActivity {
String type = _spinnerType.getSelectedItem().toString();
switch (type.toLowerCase()) {
case "totp":
case TotpInfo.ID:
case SteamInfo.ID:
_rowCounter.setVisibility(View.GONE);
_rowPeriod.setVisibility(View.VISIBLE);
break;
case "hotp":
case HotpInfo.ID:
_rowPeriod.setVisibility(View.GONE);
_rowCounter.setVisibility(View.VISIBLE);
break;
@@ -256,15 +255,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());
@@ -429,6 +419,14 @@ public class EditEntryActivity extends AegisActivity {
super.onActivityResult(requestCode, resultCode, data);
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
}
private DatabaseEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
@@ -439,7 +437,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.");
}
@@ -455,16 +453,13 @@ public class EditEntryActivity extends AegisActivity {
OtpInfo info;
try {
switch (type.toLowerCase()) {
case "totp":
int period;
try {
period = Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
info = new TotpInfo(secret, algo, digits, period);
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, parsePeriod());
break;
case "hotp":
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, parsePeriod());
break;
case HotpInfo.ID:
long counter;
try {
counter = Long.parseLong(_textCounter.getText().toString());
@@ -558,7 +553,7 @@ public class EditEntryActivity extends AegisActivity {
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equals(string)) {
if (res[i].equalsIgnoreCase(string)) {
return i;
}
}
@@ -1,17 +1,26 @@
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;
import android.content.Intent;
import android.graphics.Rect;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.Toast;
@@ -22,6 +31,8 @@ import com.beemdevelopment.aegis.ui.views.EntryListView;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import com.beemdevelopment.aegis.AegisApplication;
@@ -31,6 +42,8 @@ import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseManager;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
private static final int CODE_SCAN = 0;
@@ -48,6 +61,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private DatabaseManager _db;
private boolean _loaded;
private String _checkedGroup;
private SortCategory _sortCategory;
private Menu _menu;
private FloatingActionsMenu _fabMenu;
@@ -67,6 +81,9 @@ 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());
_entryListView.setViewMode(ViewMode.fromInteger(getPreferences().getCurrentViewMode()));
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
@@ -158,8 +175,15 @@ 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();
int viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.refresh(true);
}
}
@@ -244,6 +268,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setGroupFilter(group);
}
private void setSortCategory(SortCategory sortCategory) {
_sortCategory = sortCategory;
if(sortCategory == SortCategory.CUSTOM)
{
_entryListView.clearEntries();
loadEntries();
}
_entryListView.setSortCategory(sortCategory);
}
private void updateSortCategoryMenu(SortCategory sortCategory) {
_menu.findItem(SortCategory.getMenuItem(sortCategory)).setChecked(true);
}
private void addEntry(DatabaseEntry entry) {
_db.addEntry(entry);
_entryListView.addEntry(entry);
@@ -355,8 +395,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();
}
}
});
});
@@ -383,6 +425,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
updateSortCategoryMenu(SortCategory.fromInteger(getPreferences().getCurrentSortCategory()));
}
return true;
}
@@ -407,6 +450,37 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
setGroupFilter(group);
}
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
SortCategory sortCategory;
switch (item.getItemId()) {
case R.id.menu_sort_alphabetically:
sortCategory = SortCategory.ISSUER;
break;
case R.id.menu_sort_alphabetically_reverse:
sortCategory = SortCategory.ISSUERREVERSED;
break;
case R.id.menu_sort_alphabetically_name:
sortCategory = SortCategory.ACCOUNT;
break;
case R.id.menu_sort_alphabetically_name_reverse:
sortCategory = SortCategory.ACCOUNTREVERSED;
break;
case R.id.menu_sort_custom:
default:
sortCategory = SortCategory.CUSTOM;
break;
}
setSortCategory(sortCategory);
getPreferences().setCurrentSortCategory(sortCategory);
}
return super.onOptionsItemSelected(item);
}
}
@@ -444,11 +518,14 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
loadEntries();
SortCategory currentSortCategory = SortCategory.fromInteger(getPreferences().getCurrentSortCategory());
setSortCategory(currentSortCategory);
}
private void loadEntries() {
// load all entries
_entryListView.addEntries(_db.getEntries());
List<DatabaseEntry> entries = new ArrayList<DatabaseEntry>(_db.getEntries());
_entryListView.addEntries(entries);
_loaded = true;
}
@@ -493,4 +570,38 @@ 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;
}
}
}
@@ -2,6 +2,9 @@ package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaScannerConnection;
@@ -9,18 +12,31 @@ import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import android.text.TextUtils;
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.ViewMode;
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.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.DatabaseImporterResult;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.google.android.material.snackbar.Snackbar;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import java.io.FileNotFoundException;
@@ -46,6 +62,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 +80,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 +99,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 +135,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference exportPreference = findPreference("pref_import");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
int currentViewMode = app.getPreferences().getCurrentViewMode();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary("Selected: " + ViewMode.getViewModeName(currentViewMode));
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
String[] viewModes = ViewMode.getViewModeNames();
int checkedMode = app.getPreferences().getCurrentViewMode();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_view_mode))
.setSingleChoiceItems(viewModes, checkedMode, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
dialog.dismiss();
viewModePreference.setSummary("Selected: " + ViewMode.getViewModeName(i));
_result.putExtra("needsRecreate", true);
})
.setPositiveButton(android.R.string.ok, null)
.create());
return true;
}
});
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
@@ -101,8 +171,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 +209,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 +230,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 +380,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 +399,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 +443,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 +477,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;
}
@@ -360,7 +500,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
List<DatabaseEntry> entries = importer.convert();
DatabaseImporterResult result = importer.convert();
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();
for (DatabaseEntry entry : entries) {
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
@@ -375,7 +518,30 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
_result.putExtra("needsRecreate", true);
Toast.makeText(getActivity(), String.format(Locale.getDefault(), getString(R.string.imported_entries_count), entries.size()), Toast.LENGTH_LONG).show();
Snackbar bar = Snackbar.make(getView(), String.format(Locale.getDefault(), getString(R.string.imported_entries_count), entries.size(), errors.size()), Snackbar.LENGTH_LONG);
if (errors.size() > 0) {
bar.setAction(R.string.details, v -> {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog, which) -> {
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(getActivity(), getString(R.string.errors_copied), Toast.LENGTH_SHORT).show();
})
.create());
});
}
bar.show();
}
private void onExport() {
@@ -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);
}
@@ -5,7 +5,10 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
@@ -24,7 +27,12 @@ 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;
private SortCategory _sortCategory;
private ViewMode _viewMode;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
@@ -40,6 +48,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)) {
@@ -141,6 +157,25 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyDataSetChanged();
}
public void setSortCategory(SortCategory sortCategory) {
if (_sortCategory != sortCategory && sortCategory != SortCategory.CUSTOM) {
Collections.sort(_shownEntries, SortCategory.getComparator(sortCategory));
if(SortCategory.isReversed(sortCategory))
{
Collections.reverse(_shownEntries);
}
notifyDataSetChanged();
}
_sortCategory = sortCategory;
}
public void setViewMode(ViewMode viewMode) {
_viewMode = viewMode;
}
@Override
public void onItemDismiss(int position) {
@@ -174,7 +209,13 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry_compact, parent, false);
if (_viewMode == ViewMode.NORMAL) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
} else if (_viewMode == ViewMode.COMPACT) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry_compact, parent, false);
}
return new EntryHolder(view);
}
@@ -188,7 +229,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 +239,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 +258,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);
}
@@ -13,10 +13,13 @@ import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
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,18 +29,30 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private View _entryDivider;
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);
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_entryDivider = view.findViewById(R.id.entryDivider);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
@@ -45,7 +60,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refreshCode();
if (!isCodeHidden()) {
refreshCode();
}
_progressBar.refresh();
}
@@ -54,15 +72,22 @@ 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);
if (showProgress) {
_progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
if (_entryDivider != null) {
_entryDivider.setVisibility(View.GONE);
}
}
// only show the button if this entry is of type HotpInfo
@@ -83,7 +108,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 +135,39 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void refreshCode() {
String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
if (!isCodeHidden()) {
updateCode();
}
}
private void updateCode() {
OtpInfo info = _entry.getInfo();
String text;
if (info instanceof SteamInfo) {
text = info.getOtp();
} else {
String otp = info.getOtp();
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,8 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -13,6 +13,8 @@ import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
@@ -45,11 +47,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);
@@ -84,6 +92,17 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
runLayoutAnimation(_rvKeyProfiles);
}
public void setSortCategory(SortCategory sortCategory) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory);
runLayoutAnimation(_rvKeyProfiles);
}
public void setViewMode(ViewMode viewMode) {
_adapter.setViewMode(viewMode);
}
public void refresh(boolean hard) {
if (_showProgress) {
_progressBar.refresh();
@@ -151,6 +170,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 +218,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 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

@@ -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,137 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
android:background="?attr/cardBackground"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="16dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivTextDrawable"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_alignParentBottom="false"
android:layout_alignParentTop="false"
android:layout_alignWithParentIfMissing="false"
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="6dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/secondaryText"
android:includeFontPadding="false"
android:textSize="13sp"
android:layout_below="@+id/profile_code"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_code"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/extra_info_text"
android:textSize="13sp"
tools:text=" - AccountName" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:includeFontPadding="false"
android:textSize="26sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
</RelativeLayout>
<ImageView
android:id="@+id/buttonRefresh"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_refresh_black_24dp"
android:tint="?attr/iconColorPrimary"
android:background="?android:attr/selectableItemBackground" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:padding="0dp"
android:layout_margin="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="3dp"
android:id="@+id/progressBar"
android:max="1000"
android:layout_weight="1"/>
</LinearLayout>
<View
android:id="@+id/entryDivider"
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/divider" />
</LinearLayout>
@@ -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"/>
+29
View File
@@ -21,6 +21,35 @@
android:title="@string/all"
android:checked="true" />
</group>
</menu>
</item>
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_action_sort"
app:showAsAction="always"
android:title="@string/filter">
<menu>
<group
android:id="@+id/action_sort_category"
android:checkableBehavior="single">
<item
android:id="@+id/menu_sort_custom"
android:title="@string/sort_custom"
android:checked="true" />
<item
android:id="@+id/menu_sort_alphabetically_name"
android:title="@string/sort_alphabetically_name"/>
<item
android:id="@+id/menu_sort_alphabetically_name_reverse"
android:title="@string/sort_alphabetically_name_reverse"/>
<item
android:id="@+id/menu_sort_alphabetically"
android:title="@string/sort_alphabetically"/>
<item
android:id="@+id/menu_sort_alphabetically_reverse"
android:title="@string/sort_alphabetically_reverse"/>
</group>
</menu>
</item>
<item
+144
View File
@@ -0,0 +1,144 @@
<resources>
<string name="app_name">Aegis</string>
<string name="app_name_dev">AegisDev</string>
<string name="action_settings">Настройки</string>
<string name="action_import">Импорт</string>
<string name="action_delete">Удалить</string>
<string name="action_default_icon">Восстановить значок по умолчанию</string>
<string name="discard">Отмена</string>
<string name="save">Сохранить</string>
<string name="issuer">Эмитент</string>
<string name="title_activity_intro">IntroActivity</string>
<string name="settings">Предпочтения</string>
<string name="pref_select_theme_title">Тема</string>
<string name="pref_view_mode_title">Select view mode</string>
<string name="pref_account_name_title">Показать имя аккаунта</string>
<string name="pref_account_name_summary">Включите это, чтобы показать имя учетной записи рядом с эмитентом</string>
<string name="pref_timeout_title">Тайм-аут</string>
<string name="pref_timeout_summary">Автоматическая блокировка базы данных после %1$s секунд бездействия</string>
<string name="pref_slots_title">Ключевые слоты</string>
<string name="pref_slots_summary">Управление списком ключей которые могут расшифровать базу данных</string>
<string name="pref_import_file_title">Импорт из файла</string>
<string name="pref_import_file_summary">Импортировать базу данных из файла</string>
<string name="pref_import_app_title">Импорт из приложения</string>
<string name="pref_import_app_summary">Импорт базы данных из приложения (требуется root-доступ)</string>
<string name="pref_export_title">Экспорт</string>
<string name="pref_export_summary">Экспорт базы данных</string>
<string name="pref_secure_screen_title">Безопасность экрана</string>
<string name="pref_secure_screen_summary">Блокировка скриншотов и других попыток захвата экрана в приложении</string>
<string name="pref_tap_to_reveal_title">Нажмите, чтобы показать</string>
<string name="pref_tap_to_reveal_summary">Токены будут скрыты по умолчанию. Нажмите на токены, чтобы показать код.</string>
<string name="pref_tap_to_reveal_time_title">Тайм-аут для показа экрана</string>
<string name="pref_encryption_title">Шифрование</string>
<string name="pref_encryption_summary">Зашифруйте базу данных и разблокируйте ее с помощью пароля или отпечатка пальца</string>
<string name="pref_fingerprint_title">Отпечаток пальца</string>
<string name="pref_fingerprint_summary">Разрешить отпечатки пальцев зарегистрированных на этом устройстве, чтобы разблокировать хранилище</string>
<string name="pref_set_password_title">Изменить пароль</string>
<string name="pref_set_password_summary">Установите новый пароль который вам понадобится, чтобы разблокировать ваше хранилище</string>
<string name="fingerprint_hint">Сенсорный датчик</string>
<string name="fingerprint_not_recognized">Отпечаток пальца не распознан. Попробуйте снова.</string>
<string name="fingerprint_success">Отпечаток пальца распознан</string>
<string name="choose_authentication_method">Безопасность</string>
<string name="authentication_method_none">Нет</string>
<string name="authentication_method_none_description">Вам не нужен пароль для разблокировки хранилища, и он не будет зашифрован. Этот вариант не рекомендуется.</string>
<string name="authentication_method_password">Пароль</string>
<string name="authentication_method_password_description">Вам нужен пароль чтобы разблокировать хранилище.</string>
<string name="authentication_method_fingerprint">Отпечаток</string>
<string name="authentication_method_fingerprint_description">В дополнение к паролю для разблокировки хранилища можно использовать отпечатки пальцев, зарегистрированные на этом устройстве.</string>
<string name="authentication_method_set_password">Пароль</string>
<string name="authentication_enter_password">Введите свой пароль</string>
<string name="authentication">Разблокировать хранилище</string>
<string name="set_password">Пожалуйста, введите пароль</string>
<string name="set_group">Пожалуйста, введите название группы</string>
<string name="set_number">Пожалуйста, введите номер</string>
<string name="set_password_confirm">Пожалуйста, подтвердите пароль</string>
<string name="invalidated_fingerprint">Обнаружено изменение настроек безопасности вашего устройства. Перейдите в «Настройки - Отпечаток пальца» и повторно добавьте свой отпечаток пальца.</string>
<string-array name="authentication_methods">
<item>Нет</item>
<item>Пароль</item>
<item>Отпечаток пальца</item>
</string-array>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Привет пустой фрагмент</string>
<string name="unlock">Разблокировать</string>
<string name="advanced">Расширенные</string>
<string name="seconds">секунд</string>
<string name="counter">Счетчик</string>
<string name="digits">Цифры</string>
<string name="secret">Секрет</string>
<string name="scan">Сканировать QR-код</string>
<string name="enter_manually">Введите вручную</string>
<string name="add_fingerprint">Добавить отпечаток</string>
<string name="add_password">Добавить паспорт</string>
<string name="slots_warning">Хранилище надежно защищено. При добавлении нового отпечатка пальца на ваше устройство вы активируете аутентификацию по отпечатку пальца в Aegis.</string>
<string name="copy">Копировать</string>
<string name="edit">Изменить</string>
<string name="delete">Удалить</string>
<string name="password">Пароль</string>
<string name="confirm_password">Подтвердите пароль</string>
<string name="new_profile">Новый профиль</string>
<string name="add_new_profile">Добавить новый профиль</string>
<string name="unlock_vault_error">Не удалось разблокировать хранилище</string>
<string name="unlock_vault_error_description">Неверный пароль. Убедитесь, что вы не опечатали свой пароль.</string>
<string name="password_equality_error">Пароли должны быть одинаковыми и непустыми</string>
<string name="register_fingerprint">Зарегистрируйте свой отпечаток пальца</string>
<string name="snackbar_authentication_method">Пожалуйста, выберите метод аутентификации</string>
<string name="encrypting_vault">Шифрование хранилища</string>
<string name="delete_entry">Удалить запись</string>
<string name="delete_entry_description">Вы уверены что хотите удалить эту запись?</string>
<string name="discard_changes">Отменить изменения?</string>
<string name="discard_changes_description">Ваши изменения не были сохранены</string>
<string name="folder">Папка</string>
<string name="tap_to_select">Нажмите, чтобы выбрать</string>
<string name="saving_profile_error">Ошибка сохранения профиля</string>
<string name="welcome">Добро пожаловать</string>
<string name="app_description">Aegis - бесплатное, безопасное и открытое приложение 2FA.</string>
<string name="setup_completed">Настройка завершена</string>
<string name="setup_completed_description">Aegis настроен и готов к работе.</string>
<string name="vault_not_found">Хранилище не найдено, запуск установки…</string>
<string name="code_copied">Код скопирован в буфер обмена</string>
<string name="decryption_error">Произошла ошибка при попытке разблокировать хранилище</string>
<string name="saving_error">Произошла ошибка при попытке сохранить хранилище</string>
<string name="disable_encryption">Отключить шифрование</string>
<string name="disable_encryption_description">Вы уверены что хотите отключить шифрование? Это приведет к хранению хранилища в виде простого текста.</string>
<string name="encrypting_error">Произошла ошибка при включении шифрования</string>
<string name="permission_denied">Доступ запрещен</string>
<string name="choose_application">Выберите приложение из которого вы хотите импортировать базу данных.</string>
<string name="choose_theme">Выберите тему</string>
<string name="parsing_file_error">Произошла ошибка при попытке разобрать файл</string>
<string name="file_not_found">Ошибка: файл не найден</string>
<string name="reading_file_error">Произошла ошибка при попытке прочитать файл</string>
<string name="root_error">Ошибка: невозможно получить root-доступ</string>
<string name="imported_entries_count">Импортироаано %d записей. %d ошибки.</string>
<string name="exporting_database_error">Произошла ошибка при попытке экспорта базы данных</string>
<string name="export_database_location">База данных была экспортирована в:</string>
<string name="export_warning">Это действие экспортирует базу данных из личного хранилища Aegis.</string>
<string name="encryption_set_password_error">Произошла ошибка при попытке установить пароль:</string>
<string name="encryption_enable_fingerprint_error">Произошла ошибка при попытке разблокировать отпечаток пальца:</string>
<string name="no_cameras_available">Нет доступных камер</string>
<string name="read_qr_error">Произошла ошибка при попытке прочитать QR-код</string>
<string name="authentication_method_raw">Необработанный</string>
<string name="unlocking_vault">Разблокировка хранилища</string>
<string name="password_slot_error">У вас должен быть хотя бы один слот для пароля</string>
<string name="remove_slot">Удалить слот</string>
<string name="remove_slot_description">Вы уверены что хотите удалить этот слот?</string>
<string name="remove_group">Удалить группу</string>
<string name="remove_group_description">Вы уверены что хотите удалить эту группу? Записи в этой группе автоматически переключатся на «Нет группы».</string>
<string name="adding_new_slot_error">Произошла ошибка при попытке добавить новый слот:</string>
<string name="filter">Фильтр</string>
<string name="lock">Заблокировать</string>
<string name="all">Все</string>
<string name="name">Имя</string>
<string name="no_group">Нет группы</string>
<string name="new_group">Новая группа...</string>
<string name="enter_group_name">Введите название группы</string>
<string name="group_name_hint">Название группы</string>
<string name="preference_manage_groups">Редактировать группы</string>
<string name="preference_manage_groups_summary">Управляйте и удаляйте свои группы здесь</string>
<string name="tap_to_reveal">Скрытый</string>
</resources>
+1
View File
@@ -35,6 +35,7 @@
<color name="hint_text_dark">#a7a7a7</color>
<color name="secondary_text_dark">#FF5252</color>
<color name="background_dark">#212121</color>
<color name="background_true_dark">#000000</color>
<color name="warning_color">#f4511e</color>
<color name="hint_color">#42000000</color>
+25 -10
View File
@@ -11,20 +11,25 @@
<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_view_mode_title">View mode</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 +53,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>
@@ -60,6 +66,7 @@
<string-array name="otp_types_array">
<item>TOTP</item>
<item>HOTP</item>
<item>Steam</item>
</string-array>
<string-array name="otp_algo_array">
@@ -68,17 +75,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>
@@ -111,6 +114,7 @@
<string name="setup_completed_description">Aegis has been setup and is ready to go.</string>
<string name="vault_not_found">Vault not found, starting setup…</string>
<string name="code_copied">Code copied to the clipboard</string>
<string name="errors_copied">Errors copied to the clipboard</string>
<string name="decryption_error">An error occurred while trying to unlock the vault</string>
<string name="saving_error">An error occurred while trying to save the vault</string>
<string name="disable_encryption">Disable encryption</string>
@@ -118,10 +122,14 @@
<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="choose_view_mode">Select your desired view mode</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="imported_entries_count">Imported %d entries</string>
<string name="root_error">Error: unable to obtain root access</string>
<string name="imported_entries_count">Imported %d entries. %d errors.</string>
<string name="import_error_title">One or more errors occurred during the import</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>
<string name="export_warning">This action will export the database out of Aegis\' private storage.</string>
@@ -137,14 +145,21 @@
<string name="remove_group">Remove group</string>
<string name="remove_group_description">Are you sure you want to remove this group? Entries in this group will automatically switch to \'No group\'.</string>
<string name="adding_new_slot_error">An error occurred while trying to add a new slot:</string>
<string name="details">Details</string>
<string name="filter">Filter</string>
<string name="lock">Lock</string>
<string name="all">All</string>
<string name="name">Name</string>
<string name="no_group">No group</string>
<string name="sort_alphabetically">Issuer (A to Z)</string>
<string name="sort_alphabetically_reverse">Issuer (Z to A)</string>
<string name="sort_alphabetically_name">Account (A to Z)</string>
<string name="sort_alphabetically_name_reverse">Account (Z to A)</string>
<string name="sort_custom">Custom</string>
<string name="new_group">New group...</string>
<string name="enter_group_name">Enter a group name</string>
<string name="group_name_hint">Group name</string>
<string name="preference_manage_groups">Edit groups</string>
<string name="preference_manage_groups_summary">Manage and delete your groups here</string>
<string name="tap_to_reveal">Hidden</string>
</resources>
+14
View File
@@ -66,6 +66,20 @@
<item name="swirl_ridgeColor">@color/colorSwirlPrimaryDark</item>
<item name="swirl_errorColor">@color/colorSwirlErrorDark</item>
</style>
<style name="AppTheme.TrueBlack" parent="AppTheme.Dark">
<item name="colorPrimary">@color/background_true_dark</item>
<item name="cardBackground">@color/background_true_dark</item>
<item name="background">@color/background_true_dark</item>
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
<item name="colorPrimaryDark">@color/background_true_dark</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style>
<style name="AppTheme.TrueBlack.Preferences" parent="AppTheme.TrueBlack">
<item name="android:windowBackground">@color/background_true_dark</item>
</style>
<style name="AppTheme.Dark.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
+28 -5
View File
@@ -7,11 +7,16 @@
<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"/>
<Preference
android:defaultValue="false"
android:key="pref_view_mode"
android:title="@string/pref_view_mode_title"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
@@ -37,6 +42,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 +96,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"
@@ -6,29 +6,30 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
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);
assertEquals(_vectors[i], otp);
OTP otp = HOTP.generateOTP(_secret, "HmacSHA1", 6, i);
assertEquals(_vectors[i], otp.toString());
}
}
}
@@ -2,32 +2,51 @@ package com.beemdevelopment.aegis;
import org.junit.Test;
import com.beemdevelopment.aegis.crypto.otp.OTP;
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 +69,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 +88,8 @@ public class TOTPTest {
return;
}
String otp = TOTP.generateTOTP(seed, vector[0], 8, vector[2]);
assertEquals(vector[1], otp);
OTP otp = TOTP.generateOTP(seed, vector.Algo, 8, 30, vector.Time);
assertEquals(vector.OTP, otp.toString());
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.android.tools.build:gradle:3.3.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+39
View File
@@ -0,0 +1,39 @@
Aegis Authenticator is a free, secure and open source app to manage your
2-step verification tokens for your online services.
'''Encryption'''
All of your one-time passwords are stored in a vault. If you choose
to set a password, which is highly recommended, the vault will be encrypted
using AES-256. If someone with malicious intent gets a hold of the vault file,
its impossible for them to retrieve the contents without knowing the password.
'''Fingerprint unlock'''
Entering your password each time you need access to a
one-time password can be cumbersome. Fortunately, you can also enable
fingerprint unlock if your device has a fingerprint scanner.
'''Compatibility'''
Aegis supports the HOTP and TOTP algorithms. These two algorithms are
industry-standard and widely supported, making Aegis compatible with
thousands of services. Some examples are: Google, GitHub, Dropbox, Facebook
and Instagram.
It is also compatible with Google Authenticator. Any website that shows a QR
code for Google Authenticator also works with Aegis.
'''Groups'''
Have a lot of one-time passwords? Add them to custom groups for easier
access. Personal, Work and Social can each get their own group.
'''Backups'''
To make sure you will never lose access to your online accounts Aegis
Authenticator supports exporting your vault which you can import onto a new
device. Aegis Authenticator also allows you to import AndOTP and FreeOTP
databases so switching to Aegis is made easier for you.
'''Open source and license'''
Aegis Authenticator is open source (licensed under GPL v3) and the source code
can be found here: http://github.com/beemdevelopment/Aegis
'''Attribution'''
The icon is largely based on a design by Freepik from Flaticon.
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

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