Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f27dc54ec4 | ||
|
|
4c1dec613c | ||
|
|
a251685002 | ||
|
|
118221e3df | ||
|
|
1dd5f893da | ||
|
|
a46640f43d | ||
|
|
a293078603 | ||
|
|
f3ed79dc71 | ||
|
|
ee8fd2e9f2 | ||
|
|
8b9ec5f976 | ||
|
|
d283eae7a0 | ||
|
|
a854db760c | ||
|
|
caf2ec882a | ||
|
|
bbf967e2b7 | ||
|
|
b9d011b48e | ||
|
|
ecdadf5335 | ||
|
|
10b80442d0 | ||
|
|
a2879a4558 | ||
|
|
62025d1053 | ||
|
|
09fe550a28 | ||
|
|
07aac1e6a3 | ||
|
|
b8939b504e | ||
|
|
4f29d0c2e4 | ||
|
|
592c6683c3 | ||
|
|
f8edac72ea | ||
|
|
fc0e1150f6 | ||
|
|
e1fd913817 | ||
|
|
ede9f93a59 | ||
|
|
c6c0eb88e9 | ||
|
|
f929790be6 | ||
|
|
a4ff9d6d31 | ||
|
|
a21d28a91f | ||
|
|
f7c32bc28b | ||
|
|
59c0ca947d | ||
|
|
a7ac1adb80 | ||
|
|
17204cc026 | ||
|
|
cf4aecbd3e | ||
|
|
9baaf824c6 | ||
|
|
eb172fc54e | ||
|
|
e99762b814 | ||
|
|
6b1d4a60e3 | ||
|
|
a540b3cfee | ||
|
|
b0481d6b30 | ||
|
|
0c315c53e5 | ||
|
|
75c37a454f | ||
|
|
554d7e17be |
+2
-2
@@ -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
@@ -1 +0,0 @@
|
||||
Aegis
|
||||
Generated
-22
@@ -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>
|
||||
Generated
-3
@@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
Generated
-6
@@ -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>
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<compositeConfiguration>
|
||||
<compositeBuild compositeDefinitionSource="SCRIPT" />
|
||||
</compositeConfiguration>
|
||||
<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>
|
||||
Generated
-33
@@ -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>
|
||||
Generated
-9
@@ -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>
|
||||
Generated
-12
@@ -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
@@ -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>
|
||||
@@ -14,9 +14,12 @@ Aegis is a free, secure and open source 2FA app for Android.
|
||||
- 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))
|
||||
@@ -26,15 +29,17 @@ 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="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).png
|
||||
[<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="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)
|
||||
@@ -62,3 +67,7 @@ There are plans to make it available on F-Droid as well.
|
||||
## 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.
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 3
|
||||
versionName "0.2"
|
||||
versionCode 6
|
||||
versionName "0.3"
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
|
||||
@@ -39,10 +39,38 @@ public class Preferences {
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class HOTP {
|
||||
private HOTP() {
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
|
||||
|
||||
@@ -30,18 +30,11 @@ public class HOTP {
|
||||
// truncate hash to get the HTOP value
|
||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||
int offset = hash[hash.length - 1] & 0xf;
|
||||
long bin = ((hash[offset] & 0x7f) << 24)
|
||||
int otp = ((hash[offset] & 0x7f) << 24)
|
||||
| ((hash[offset + 1] & 0xff) << 16)
|
||||
| ((hash[offset + 2] & 0xff) << 8)
|
||||
| (hash[offset + 3] & 0xff);
|
||||
long otp = bin % (long) Math.pow(10, digits);
|
||||
|
||||
// prepend zeroes if needed
|
||||
StringBuilder res = new StringBuilder(Long.toString(otp));
|
||||
while (res.length() < digits) {
|
||||
res.insert(0, "0");
|
||||
}
|
||||
|
||||
return res.toString();
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ public class TOTP {
|
||||
private TOTP() {
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
|
||||
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);
|
||||
}
|
||||
|
||||
public static String generateOTP(byte[] secret, String algo, int digits, long period)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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());
|
||||
}
|
||||
}
|
||||
+13
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@ package com.beemdevelopment.aegis.importers;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
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.encoding.Base64Exception;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
|
||||
public class AegisFileImporter extends DatabaseFileImporter {
|
||||
@@ -33,7 +33,9 @@ public class AegisFileImporter extends DatabaseFileImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseEntry> convert() throws DatabaseImporterException {
|
||||
public DatabaseImporterResult convert() throws DatabaseImporterException {
|
||||
DatabaseImporterResult result = new DatabaseImporterResult();
|
||||
|
||||
try {
|
||||
JSONObject obj;
|
||||
if (_file.isEncrypted() && _creds != null) {
|
||||
@@ -42,11 +44,29 @@ public class AegisFileImporter extends DatabaseFileImporter {
|
||||
obj = _file.getContent();
|
||||
}
|
||||
|
||||
Database db = Database.fromJson(obj);
|
||||
return db.getEntries();
|
||||
} catch (DatabaseException | DatabaseFileException e) {
|
||||
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
|
||||
|
||||
@@ -6,9 +6,7 @@ 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 java.nio.charset.StandardCharsets;
|
||||
|
||||
import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base32;
|
||||
@@ -16,6 +14,7 @@ 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;
|
||||
|
||||
@@ -29,55 +28,68 @@ public class AndOtpFileImporter extends DatabaseFileImporter {
|
||||
@Override
|
||||
public void parse() throws DatabaseImporterException {
|
||||
try {
|
||||
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
|
||||
_obj = new JSONArray(new String(_stream.getBytes(), StandardCharsets.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<>();
|
||||
public DatabaseImporterResult convert() throws DatabaseImporterException {
|
||||
DatabaseImporterResult result = new DatabaseImporterResult();
|
||||
|
||||
try {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
for (int i = 0; i < _obj.length(); i++) {
|
||||
try {
|
||||
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);
|
||||
DatabaseEntry entry = convertEntry(obj);
|
||||
result.addEntry(entry);
|
||||
} catch (JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} catch (DatabaseImporterEntryException e) {
|
||||
result.addError(e);
|
||||
}
|
||||
} catch (Base32Exception | OtpInfoException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
}
|
||||
|
||||
return entries;
|
||||
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
|
||||
|
||||
@@ -16,6 +16,7 @@ public abstract class DatabaseAppImporter implements DatabaseImporter {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ public abstract class DatabaseAppImporter implements DatabaseImporter {
|
||||
|
||||
public abstract void parse() throws DatabaseImporterException;
|
||||
|
||||
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
|
||||
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public abstract class DatabaseFileImporter implements DatabaseImporter {
|
||||
|
||||
public abstract void parse() throws DatabaseImporterException;
|
||||
|
||||
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
|
||||
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
|
||||
|
||||
public abstract boolean isEncrypted();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.List;
|
||||
|
||||
public interface DatabaseImporter {
|
||||
void parse() throws DatabaseImporterException;
|
||||
List<DatabaseEntry> convert() throws DatabaseImporterException;
|
||||
DatabaseImporterResult convert() throws DatabaseImporterException;
|
||||
boolean isEncrypted();
|
||||
Context getContext();
|
||||
}
|
||||
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -46,43 +46,48 @@ public class FreeOtpFileImporter extends DatabaseFileImporter {
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@@ -32,7 +32,7 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
|
||||
@SuppressLint("SdCardPath")
|
||||
private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases";
|
||||
|
||||
private List<DatabaseEntry> _entries = new ArrayList<>();
|
||||
private List<Entry> _entries = new ArrayList<>();
|
||||
|
||||
public GoogleAuthAppImporter(Context context) {
|
||||
super(context);
|
||||
@@ -61,34 +61,11 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
|
||||
}
|
||||
|
||||
do {
|
||||
int type = getInt(cursor, "type");
|
||||
byte[] secret = Base32.decode(getString(cursor, "secret").toCharArray());
|
||||
|
||||
OtpInfo info;
|
||||
switch (type) {
|
||||
case TYPE_TOTP:
|
||||
info = new TotpInfo(secret);
|
||||
break;
|
||||
case TYPE_HOTP:
|
||||
info = new HotpInfo(secret, getInt(cursor, "counter"));
|
||||
break;
|
||||
default:
|
||||
throw new DatabaseImporterException("unsupported otp type: " + type);
|
||||
}
|
||||
|
||||
String name = getString(cursor, "email", "");
|
||||
String issuer = getString(cursor, "issuer", "");
|
||||
|
||||
String[] parts = name.split(":");
|
||||
if (parts.length == 2) {
|
||||
name = parts[1];
|
||||
}
|
||||
|
||||
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
|
||||
Entry entry = new Entry(cursor);
|
||||
_entries.add(entry);
|
||||
} while(cursor.moveToNext());
|
||||
}
|
||||
} catch (SQLiteException | OtpInfoException | Base32Exception e) {
|
||||
} catch (SQLiteException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
} finally {
|
||||
// always delete the temporary file
|
||||
@@ -97,8 +74,47 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DatabaseEntry> convert() {
|
||||
return _entries;
|
||||
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
|
||||
@@ -121,4 +137,44 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
|
||||
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,6 +10,8 @@ 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, long counter) throws OtpInfoException {
|
||||
@@ -28,7 +31,8 @@ public class HotpInfo extends OtpInfo {
|
||||
@Override
|
||||
public String getOtp() {
|
||||
try {
|
||||
return HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
|
||||
return otp.toString();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
@@ -36,7 +40,7 @@ public class HotpInfo extends OtpInfo {
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "hotp";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -98,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);
|
||||
@@ -126,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,5 +1,6 @@
|
||||
package com.beemdevelopment.aegis.otp;
|
||||
|
||||
import com.beemdevelopment.aegis.crypto.otp.OTP;
|
||||
import com.beemdevelopment.aegis.crypto.otp.TOTP;
|
||||
|
||||
import org.json.JSONException;
|
||||
@@ -9,6 +10,8 @@ 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 {
|
||||
@@ -24,7 +27,8 @@ public class TotpInfo extends OtpInfo {
|
||||
@Override
|
||||
public String getOtp() {
|
||||
try {
|
||||
return TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
|
||||
return otp.toString();
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -32,7 +36,7 @@ public class TotpInfo extends OtpInfo {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -166,7 +167,7 @@ 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);
|
||||
@@ -189,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;
|
||||
@@ -253,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());
|
||||
@@ -426,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.");
|
||||
@@ -452,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());
|
||||
@@ -555,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ 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;
|
||||
@@ -27,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;
|
||||
@@ -37,7 +43,6 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
public class MainActivity extends AegisActivity implements EntryListView.Listener {
|
||||
// activity request codes
|
||||
@@ -56,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;
|
||||
@@ -77,6 +83,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_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);
|
||||
@@ -170,10 +177,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,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);
|
||||
@@ -369,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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -397,6 +425,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
updateLockIcon();
|
||||
if (_loaded) {
|
||||
updateGroupFilterMenu();
|
||||
updateSortCategoryMenu(SortCategory.fromInteger(getPreferences().getCurrentSortCategory()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -421,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);
|
||||
}
|
||||
}
|
||||
@@ -458,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;
|
||||
}
|
||||
|
||||
@@ -527,8 +590,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
super.onAnimationEnd(animation);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !isAnimating) {
|
||||
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !isAnimating) {
|
||||
_fabMenu.setVisibility(View.VISIBLE);
|
||||
_fabMenu.animate()
|
||||
.translationY(0)
|
||||
|
||||
@@ -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,11 +12,18 @@ 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;
|
||||
@@ -21,9 +31,12 @@ 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;
|
||||
@@ -86,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) {
|
||||
@@ -96,6 +135,33 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
@@ -434,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) {
|
||||
@@ -449,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;
|
||||
@@ -27,6 +30,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
|
||||
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;
|
||||
@@ -151,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) {
|
||||
|
||||
@@ -184,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ 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;
|
||||
@@ -27,6 +29,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private DatabaseEntry _entry;
|
||||
private ImageView _buttonRefresh;
|
||||
|
||||
private View _entryDivider;
|
||||
|
||||
private View _currentView;
|
||||
|
||||
private boolean _hidden;
|
||||
@@ -47,6 +51,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_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);
|
||||
@@ -78,6 +84,10 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
_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
|
||||
@@ -131,10 +141,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
private void updateCode() {
|
||||
String otp = _entry.getInfo().getOtp();
|
||||
String text = otp.substring(0, (otp.length() / 2)
|
||||
+ (otp.length() % 2)) + " "
|
||||
+ otp.substring(otp.length() / 2);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -15,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;
|
||||
@@ -35,8 +35,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
||||
|
||||
private UiRefresher _refresher;
|
||||
|
||||
private RecyclerView.OnScrollChangeListener _onScrollListener;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -49,8 +47,6 @@ 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);
|
||||
@@ -96,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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -35,6 +35,7 @@
|
||||
<color name="hint_text_dark">#a7a7a7</color>
|
||||
<color name="secondary_text_dark">#FF5252</color>
|
||||
<color name="background_dark">#212121</color>
|
||||
<color name="background_true_dark">#000000</color>
|
||||
|
||||
<color name="warning_color">#f4511e</color>
|
||||
<color name="hint_color">#42000000</color>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<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>
|
||||
@@ -66,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">
|
||||
@@ -113,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>
|
||||
@@ -120,11 +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="root_error">Error: unable to obtain root access</string>
|
||||
<string name="imported_entries_count">Imported %d entries</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>
|
||||
@@ -140,11 +145,17 @@
|
||||
<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>
|
||||
|
||||
@@ -66,6 +66,20 @@
|
||||
<item name="swirl_ridgeColor">@color/colorSwirlPrimaryDark</item>
|
||||
<item name="swirl_errorColor">@color/colorSwirlErrorDark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.TrueBlack" parent="AppTheme.Dark">
|
||||
<item name="colorPrimary">@color/background_true_dark</item>
|
||||
<item name="cardBackground">@color/background_true_dark</item>
|
||||
<item name="background">@color/background_true_dark</item>
|
||||
<item name="bottomSheetDialogTheme">@style/Theme.Design.BottomSheetDialog</item>
|
||||
<item name="colorPrimaryDark">@color/background_true_dark</item>
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.TrueBlack.Preferences" parent="AppTheme.TrueBlack">
|
||||
<item name="android:windowBackground">@color/background_true_dark</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Dark.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
|
||||
@@ -7,11 +7,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
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.*;
|
||||
|
||||
@@ -27,8 +28,8 @@ public class HOTPTest {
|
||||
@Test
|
||||
public void vectorsMatch() throws InvalidKeyException, NoSuchAlgorithmException {
|
||||
for (int i = 0; i < _vectors.length; i++) {
|
||||
String otp = HOTP.generateOTP(_secret, "HmacSHA1", 6, i);
|
||||
assertEquals(_vectors[i], otp);
|
||||
OTP otp = HOTP.generateOTP(_secret, "HmacSHA1", 6, i);
|
||||
assertEquals(_vectors[i], otp.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -87,8 +88,8 @@ public class TOTPTest {
|
||||
return;
|
||||
}
|
||||
|
||||
String otp = TOTP.generateOTP(seed, vector.Algo, 8, 30, vector.Time);
|
||||
assertEquals(vector.OTP, otp);
|
||||
OTP otp = TOTP.generateOTP(seed, vector.Algo, 8, 30, vector.Time);
|
||||
assertEquals(vector.OTP, otp.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,10 @@ 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 |
Reference in New Issue
Block a user