Compare commits

...
16 Commits
Author SHA1 Message Date
Alexander Bakker 2c0068bea7 Release v1.4.2
Build / build (push) Canceled after 0s
2021-01-16 15:27:00 +01:00
Alexander BakkerandGitHub 6dcde38667 Merge pull request #664 from alexbakker/downgrade-zip4j
Downgrade zip4j to 2.6.0
2021-01-16 15:10:39 +01:00
Michael SchättgenandGitHub ec1913c77a Merge pull request #661 from alexbakker/fix-660
Use name() instead of toString() on StandardCharsets.UTF_8
2021-01-13 20:44:07 +01:00
Michael SchättgenandGitHub 3e40cc9c8f Merge pull request #655 from alexbakker/phonefactor-error
Show a clearer error message when encountering phonefactor:// URI's
2021-01-13 20:24:26 +01:00
Michael SchättgenandGitHub 40d1e22079 Merge pull request #643 from alexbakker/documentsui-error
Simplify the auto lock block logic and show an error if DocumentsUI is missing
2021-01-13 20:22:57 +01:00
Michael SchättgenandGitHub 38bbb60414 Merge pull request #652 from alexbakker/fix-authy-import
Don't rely on accountType to detect proprietary Authy tokens
2021-01-13 20:11:22 +01:00
Alexander Bakker 98d39ee399 Downgrade zip4j to 2.6.0
It looks like we broke the Authenticator Plus importer in
d660fbc6d1 by upgrading zip4j. So this patch
downgrades it again. I couldn't immediately find this issue in their issue
tracker, so it may be worth spending some time reporting this to them later.
2021-01-12 21:27:24 +01:00
Alexander Bakker 737b9da292 Use name() instead of toString() on StandardCharsets.UTF_8
Apparently toString() doesn't produce the same result on every platform.
2021-01-08 17:38:02 +01:00
Alexander BakkerandGitHub dea13f56f5 Merge pull request #659 from deadmeu/fix-fullscreen-styles
Add dark theme variants of the fullscreen style
2021-01-07 20:11:55 +01:00
deadmeu 1f0c51b45b Add dark theme variants of the fullscreen style 2021-01-08 00:27:16 +10:00
Alexander Bakker 466e296988 Fix indentation of feature list format of the F-Droid app description 2021-01-06 17:47:39 +01:00
Alexander Bakker fb17f48013 Fix feature list format of the F-Droid app description 2021-01-06 17:30:36 +01:00
Alexander Bakker f203eab3ef Update F-Droid app description 2021-01-06 16:28:56 +01:00
Alexander Bakker 0a6c89cd9d Show a clearer error message when encountering phonefactor:// URI's
Sometimes users get confused when they're trying to scan a Microsoft Authenticator QR code and think the error occurs because of a bug in Aegis.
2021-01-03 19:58:39 +01:00
Alexander Bakker 951241e95d Don't rely on accountType to detect proprietary Authy tokens 2021-01-02 14:55:26 +01:00
Alexander Bakker 4482a21a54 Simplify the auto lock block logic and show an error if DocumentsUI is missing 2020-12-27 20:24:40 +01:00
14 changed files with 172 additions and 69 deletions
+4 -3
View File
@@ -24,8 +24,8 @@ android {
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 30
versionCode 44
versionName "1.4.1"
versionCode 45
versionName "1.4.2"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
@@ -152,7 +152,8 @@ dependencies {
implementation 'com.nulab-inc:zxcvbn:1.3.1'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation 'net.lingala.zip4j:zip4j:2.6.4'
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
+10
View File
@@ -31,6 +31,16 @@
</head>
<body>
<div></div>
<h3>Version 1.4.2</h3>
<h4>Fixes</h4>
<ul>
<li>The app would crash if DocumentsUI is not present on the device</li>
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
<li>Importing from Authy was flaky for entries that have an icon</li>
<li>The dark theme was not properly applied to the QR code scanner view</li>
<li>The app would crash on plain text export on some devices</li>
<li>Importing from Authenticator Plus stopped working</li>
</ul>
<h3>Version 1.4.1</h3>
<h4>Fixes</h4>
<ul>
@@ -20,4 +20,10 @@ public class ThemeMap {
Theme.DARK, R.style.AppTheme_Dark_NoActionBar,
Theme.AMOLED, R.style.AppTheme_TrueBlack_NoActionBar
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.AppTheme_Fullscreen,
Theme.DARK, R.style.AppTheme_Fullscreen_Dark,
Theme.AMOLED, R.style.AppTheme_Fullscreen_TrueBlack
);
}
@@ -235,7 +235,7 @@ public class AuthyImporter extends DatabaseImporter {
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
boolean isAuthy = !entry.optString("accountType", "authy").equals("authenticator");
boolean isAuthy = entry.has("secretSeed");
sanitizeEntryInfo(authyEntryInfo, isAuthy);
byte[] secret;
@@ -246,7 +246,7 @@ public class AuthyImporter extends DatabaseImporter {
}
int digits = entry.getInt("digits");
OtpInfo info = new TotpInfo(secret, "SHA1", digits, digits == 7 ? 10 : 30);
OtpInfo info = new TotpInfo(secret, "SHA1", digits, isAuthy ? 10 : 30);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
@@ -65,7 +65,7 @@ public class GoogleAuthInfo implements Serializable {
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
@@ -73,27 +73,27 @@ public class GoogleAuthInfo implements Serializable {
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
}
byte[] secret;
try {
secret = parseSecret(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException("Bad secret", e);
throw new GoogleAuthInfoException(uri, "Bad secret", e);
}
OtpInfo info;
try {
String type = uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
@@ -117,16 +117,16 @@ public class GoogleAuthInfo implements Serializable {
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
// provider info used to disambiguate accounts
@@ -166,7 +166,7 @@ public class GoogleAuthInfo implements Serializable {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
return new GoogleAuthInfo(info, accountName, issuer);
@@ -183,7 +183,7 @@ public class GoogleAuthInfo implements Serializable {
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("Bad URI format");
throw new GoogleAuthInfoException(uri, "Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
@@ -191,17 +191,17 @@ public class GoogleAuthInfo implements Serializable {
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException("Unsupported host");
throw new GoogleAuthInfoException(uri, "Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException("Parameter 'data' is not set");
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
@@ -209,7 +209,7 @@ public class GoogleAuthInfo implements Serializable {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
List<GoogleAuthInfo> infos = new ArrayList<>();
@@ -227,7 +227,7 @@ public class GoogleAuthInfo implements Serializable {
digits = 8;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported digits: %d", params.getDigits().ordinal()));
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
}
String algo;
@@ -244,7 +244,7 @@ public class GoogleAuthInfo implements Serializable {
algo = "SHA512";
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
}
byte[] secret = params.getSecret().toByteArray();
@@ -258,10 +258,10 @@ public class GoogleAuthInfo implements Serializable {
otp = new HotpInfo(secret, algo, digits, params.getCounter());
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e){
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
String name = params.getName();
@@ -1,16 +1,30 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public class GoogleAuthInfoException extends Exception {
public GoogleAuthInfoException(Throwable cause) {
private final Uri _uri;
public GoogleAuthInfoException(Uri uri, Throwable cause) {
super(cause);
_uri = uri;
}
public GoogleAuthInfoException(String message) {
public GoogleAuthInfoException(Uri uri, String message) {
super(message);
_uri = uri;
}
public GoogleAuthInfoException(String message, Throwable cause) {
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
super(message, cause);
_uri = uri;
}
/**
* Reports whether the scheme of the URI is phonefactor://.
*/
public boolean isPhoneFactor() {
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
}
@Override
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}
@@ -1,11 +1,14 @@
package com.beemdevelopment.aegis.ui;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.AegisApplication;
@@ -55,12 +58,32 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
super.onDestroy();
}
@CallSuper
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
protected void onResume() {
super.onResume();
_app.setBlockAutoLock(false);
}
@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle bundle) {
if (isAutoLockBypassedForAction(intent.getAction())) {
_app.setBlockAutoLock(true);
}
try {
super.startActivityForResult(intent, requestCode, bundle);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(this, R.string.documentsui_error, e);
} else {
throw e;
}
}
}
@Override
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
@@ -135,4 +158,17 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
protected boolean isOrphan() {
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
private static boolean isDocsAction(@Nullable String action) {
return action != null && (action.equals(Intent.ACTION_GET_CONTENT)
|| action.equals(Intent.ACTION_CREATE_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT_TREE));
}
private static boolean isAutoLockBypassedForAction(@Nullable String action) {
return isDocsAction(action) || (action != null && (action.equals(Intent.ACTION_PICK)
|| action.equals(Intent.ACTION_SEND)
|| action.equals(Intent.ACTION_CHOOSER)));
}
}
@@ -407,8 +407,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void startScanImageActivity() {
_app.setBlockAutoLock(true);
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
@@ -191,8 +191,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
_app.setBlockAutoLock(true);
startActivityForResult(intent, CODE_IMPORT);
});
return true;
@@ -737,8 +735,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
_app.setBlockAutoLock(true);
startActivityForResult(intent, requestCode);
});
@@ -775,8 +771,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_STREAM, uri);
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
_app.setBlockAutoLock(true);
startActivity(chooser);
});
});
@@ -1019,8 +1013,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
_app.setBlockAutoLock(true);
startActivityForResult(intent, CODE_BACKUPS);
}
@@ -17,6 +17,7 @@ import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
@@ -77,7 +78,7 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
@Override
protected void onSetTheme() {
setTheme(R.style.AppTheme_Fullscreen);
setTheme(ThemeMap.FULLSCREEN);
}
@Override
@@ -156,7 +157,9 @@ public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Lis
}
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, ((dialog, which) -> bindPreview(_cameraProvider)));
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
_cameraProvider.unbindAll();
}
}
@@ -173,7 +173,7 @@ public class VaultManager {
* Google Authenticator URI's and writing it to the given OutputStream.
*/
public void exportGoogleUris(OutputStream outStream) throws VaultManagerException {
try (PrintStream stream = new PrintStream(outStream, false, StandardCharsets.UTF_8.toString())) {
try (PrintStream stream = new PrintStream(outStream, false, StandardCharsets.UTF_8.name())) {
for (VaultEntry entry : getEntries()) {
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
stream.println(info.getUri().toString());
+2
View File
@@ -168,6 +168,7 @@
<string name="disable_encryption_error">An error occurred while disabling encryption</string>
<string name="backup_successful">The backup was scheduled successfully</string>
<string name="backup_error">An error occurred while trying to create a backup</string>
<string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string>
<string name="permission_denied">Permission denied</string>
<string name="andotp_new_format">New format (v0.6.3 or newer) </string>
<string name="andotp_old_format">Old format (v0.6.2 or older) </string>
@@ -192,6 +193,7 @@
<string name="encryption_enable_biometrics_error">An error occurred while trying to enable biometric unlock. Some devices have poor implementations of biometric authentication and it is likely that yours is one of them. Consider switching to a password-only configuration instead.</string>
<string name="no_cameras_available">No cameras available</string>
<string name="read_qr_error">An error occurred while trying to read the QR code</string>
<string name="read_qr_error_phonefactor">Aegis is not compatible with Microsoft\'s proprietary 2FA algorithm. Please make sure to select \"Setup application without notifications\" when configuring 2FA in Office 365.</string>
<string name="authentication_method_raw">Raw</string>
<string name="unlocking_vault">Unlocking the vault</string>
<string name="unlocking_vault_repair">Unlocking the vault (repairing)</string>
+24
View File
@@ -126,6 +126,30 @@
<item name="android:windowFullscreen">true</item>
</style>
<style name="AppTheme.Fullscreen.Dark" parent="AppTheme.Dark">
<!-- AppTheme.TransparentActionBar-->
<item name="android:actionBarStyle">@style/ThemeActionBar</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="actionBarStyle">@style/ThemeActionBar</item>
<item name="windowActionBarOverlay">true</item>
<!-- AppTheme.Fullscreen-->
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
<style name="AppTheme.Fullscreen.TrueBlack" parent="AppTheme.TrueBlack">
<!-- AppTheme.TransparentActionBar-->
<item name="android:actionBarStyle">@style/ThemeActionBar</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="actionBarStyle">@style/ThemeActionBar</item>
<item name="windowActionBarOverlay">true</item>
<!-- AppTheme.Fullscreen-->
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
<style name="AppTheme.Dark" parent="Theme.AppCompat">
<item name="primaryText">@color/primary_text_dark</item>
<item name="secondaryText">@color/secondary_text_dark</item>
+44 -28
View File
@@ -1,36 +1,52 @@
Aegis Authenticator is a free, secure and open source app to manage your
2-step verification tokens for your online services.
<b>Encryption</b>
All of your one-time passwords are stored in a vault. If you choose
to set a password, which is highly recommended, the vault will be encrypted
using AES-256. If someone with malicious intent gets a hold of the vault file,
its impossible for them to retrieve the contents without knowing the password.
<b>Biometric unlock</b>
Entering your password each time you need access to a
one-time password can be cumbersome. Fortunately, you can also enable
biometric unlock if your device has a biometrics sensor.
Aegis Authenticator is a free, secure and open source app to manage your 2-step verification tokens for your online services.
<b>Compatibility</b>
Aegis supports the HOTP and TOTP algorithms. These two algorithms are
industry-standard and widely supported, making Aegis compatible with
thousands of services. Some examples are: Google, GitHub, Dropbox, Facebook
and Instagram.
Aegis supports the HOTP and TOTP algorithms. These two algorithms are industry-standard and widely supported, making Aegis compatible with thousands of services. Any web service that supports Google Authenticator will also work with Aegis Authenticator.
It is also compatible with Google Authenticator. Any website that shows a QR
code for Google Authenticator also works with Aegis.
<b>Encryption and biometric unlock</b>
All of your one-time passwords are stored in a vault. If you choose to set a password (highly recommended), the vault will be encrypted using strong cryptography. If someone with malicious intent gets a hold of the vault file, its impossible for them to retrieve the contents without knowing the password. Entering your password each time you need access to a one-time password can be cumbersome. Fortunately, you can also enable biometric unlock if your device has a biometrics sensor (i.e. fingerprint or face unlock).
<b>Groups</b>
Have a lot of one-time passwords? Add them to custom groups for easier
access. Personal, Work and Social can each get their own group.
<b>Organization</b>
Over time, you'll likely accumulate tens of entries in your vault. Aegis Authenticator has lots of organization options to make finding the one you need at a particular moment easier. Set a custom icon for an entry to make it easier to find. Search by account name or service name. Have a lot of one-time passwords? Add them to custom groups for easier access. Personal, Work and Social can each get their own group.
<b>Backups</b>
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.
To make sure you will never lose access to your online accounts, Aegis Authenticator can create automatic backups of the vault to a location of your choosing. If your cloud provider supports the Storage Access Framework of Android (like Nextcloud does), it can even create automatic backups to the cloud. Creating manual exports of the vault is also supported.
<b>Making the switch</b>
To make the switch easier, Aegis Authenticator can import the entries of lots of other authenticators, including: Authenticator Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft Authenticator, Steam, TOTP Authenticator and WinAuth (root access is required for the apps that don't have an option to export).
<b>Feature overview</b>
<ul>
<li>Free and open source</li>
<li>Secure
<ul>
<li>Encrypted, can be unlocked with a password or biometrics</li>
<li>Screen capture prevention</li>
<li>Tap to reveal</li>
</ul>
</li>
<li>Compatible with Google Authenticator</li>
<li>Supports industry standard algorithms: HOTP and TOTP</li>
<li>Lots of ways to add new entries
<ul>
<li>Scan a QR code or an image of one</li>
<li>Enter details manually</li>
<li>Import from other popular authenticator apps</li>
</ul>
</li>
<li>Organization
<ul>
<li>Alphabetic/custom sorting</li>
<li>Custom or automatically generated icons</li>
<li>Group entries together</li>
<li>Advanced entry editing</li>
<li>Search by name/issuer</li>
</ul>
</li>
<li>Material design with multiple themes: Light, Dark, AMOLED</li>
<li>Export (plaintext or encrypted)</li>
<li>Automatic backups of the vault to a location of your choosing</li>
</ul>
<b>Open source and license</b>
Aegis Authenticator is open source (licensed under GPL v3) and the source code
can be found here: http://github.com/beemdevelopment/Aegis
Aegis Authenticator is open source (licensed under GPL v3) and the source code can be found here: http://github.com/beemdevelopment/Aegis