Compare commits

...
57 Commits
Author SHA1 Message Date
Alexander Bakker 99c222cffa Release v0.4 2019-05-21 21:48:03 +02:00
Alexander BakkerandMichael Schättgen 57e3574693 Fix a bug where Aegis would forcibly pop up after unlocking the device 2019-05-20 22:27:28 +02:00
Alexander BakkerandGitHub 98c166a6b2 Merge pull request #86 from michaelschattgen/feature-clearpassword
Select and focus password on failed unlock attempt
2019-05-20 20:47:10 +02:00
Michael Schättgen b6a31e798f Select and focus password on failed unlock attempt 2019-05-20 20:41:56 +02:00
Michael Schättgen 5ebb714d05 Fix setting the preferred theme 2019-05-15 22:23:13 +02:00
Alexander BakkerandMichael Schättgen 9958f73926 Set the Activity theme before calling super.onCreate (#84)
This fixes a NullPointerException that would occur when restoring from
savedInstanceState due to getSupportActionBar returning null. It also removes
the theme definitions from AndroidManifest as we override those anyway.
2019-05-15 22:01:00 +02:00
Alexander BakkerandMichael Schättgen 0fb33744ae Update Krop to 0.44 (#83) 2019-05-15 21:56:14 +02:00
Alexander BakkerandMichael Schättgen 6d26d1beb0 Fix the last couple of sorting bugs (#77)
This fixes the following bugs:
- Sort category is forgotten after lock/unlock
- The sort mode is not respected for new entries

I got a little carried away while working on this patch and also included the
following other enhancements:
- Simplify the SortCategory, Theme and ViewMode enums
- Simplify usage of string resources
- Don't call notifyDataSetChanged and runLayoutAnimation unnecessarily
2019-05-15 21:29:45 +02:00
Alexander Bakker 0a8dd56306 Escape apostrophes in the French translation 2019-05-15 19:54:49 +02:00
mdmkandMichael Schättgen 8a8d714901 French translation and small changes to english strings. (#59)
* Typos in strings

- Double space.
- Missing verb in sentence. "need" makes sense here.
- Equal is a verb suitable for speaking of strings. For a final user, identical is more understandable.

* French translation
2019-05-15 10:45:08 +02:00
Alexander BakkerandGitHub c2cb5a3044 Merge pull request #74 from alexbakker/feature-encrypted-andotp
Add support for importing encrypted andOTP backups
2019-05-01 14:46:39 +02:00
Alexander Bakker 9893b2f057 Add support for importing encrypted andOTP backups
This also refactors dialog_newgroup to serve as a generic text input dialog
2019-05-01 14:42:39 +02:00
Alexander BakkerandGitHub 83b2f85ff4 Merge pull request #73 from alexbakker/refactor-import
Refactor database importer logic
2019-05-01 14:34:18 +02:00
Alexander Bakker 47df8968c7 Refactor database importer logic
Summary:
- Combine app and file importers into one type
- Split encrypted and plain text file logic into separate DatabaseImporter.State
  classes
- Add an abstract FileReader class that can handle reading from apps and files

The idea is that every importer implements its own UI logic to obtain
credentials for decryption through a dialog. Aegis databases are still an
exception to this rule as it still uses an Activity to ask for credentials. This
will be addressed in a future patch.

As a result of this change all app importers are now also available under
"Import from file".
2019-05-01 14:30:57 +02:00
Alexander BakkerandGitHub 33b4b4b7ab Merge pull request #72 from michaelschattgen/feature-selectentries
Add ability to select entries when importing
2019-05-01 13:14:16 +02:00
Michael SchättgenandAlexander Bakker 21fd8fdd8d Add ability to select entries when importing
Move onScroll to seperate class to avoid duplicate code

Move importing logic back to the PreferencesFragment

Add minor changes

Add ImportEntry to properly track checked states

Minor layout changes
2019-05-01 13:13:50 +02:00
Michael SchättgenandGitHub 7ec8391872 Merge pull request #71 from alexbakker/fix-pass-encode
Fix a bug where the password encode function added trailing null bytes
2019-04-30 15:20:01 +02:00
Alexander Bakker afb9e59711 Fix a bug where the password encode function added trailing null bytes
Funny story. Instead of	obtaining the actual bytes from the ByteBuffer in the
password encode function, we obtained the entire buffer. This caused some
trailing null bytes to be added to the encoded password. Luckily (and
strangely), PBKDF2 produces collisions for inputs with trailing null bytes and
thus scrypt does this as well. As such, this bug doesn't affect us right now,
but it would if we were to use that encode function for other purposes in the
future.

This also adds a test that checks for the expected collision behavior of scrypt.
2019-04-24 13:43:46 +02:00
Alexander BakkerandGitHub 24a93ecc9f Merge pull request #66 from michaelschattgen/feature-scanimage
Add ability to scan image for QR code
2019-04-23 21:09:15 +02:00
Michael Schättgen 2de9b96138 Add ability to scan image for QR code 2019-04-23 20:27:31 +02:00
Michael SchättgenandGitHub e0d5aba94a Update README.md 2019-04-18 17:02:56 +02:00
Michael SchättgenandGitHub f3b4433d5b Merge pull request #60 from alexbakker/doc-vault
Add documentation for Aegis' security design and vault format
2019-04-18 14:43:17 +02:00
Alexander Bakker f4678e2813 Release v0.3.3 2019-04-17 13:40:57 +02:00
Michael Schättgen b958145761 Fix bug where parsing otp codes was impossible
Y I K E S
2019-04-17 13:37:53 +02:00
Alexander Bakker feea9a2e67 Fix crash when trying to auto lock a locked vault 2019-04-17 08:45:42 +02:00
Alexander Bakker ebb9d0be3f Make app importer path lookup more dynamic
Also fixes a crash in SteamAppImporter that occurred with empty dirs
2019-04-17 01:32:20 +02:00
Alexander Bakker f5cbec21f4 Release v0.3.2 2019-04-17 00:00:41 +02:00
Alexander Bakker e55d6111cf Fix bold in full_description.txt for F-Droid 2019-04-17 00:00:10 +02:00
Alexander Bakker cb01fb21d3 Remove extra translations to fix lint errors in release mode 2019-04-16 23:48:50 +02:00
Alexander Bakker c5be99e58d Release v0.3.1 2019-04-16 23:38:05 +02:00
Alexander Bakker 92458d0d3b Move setGlobalDurationScale to AegisActivity
Also, rename the method and make the error string translatable
2019-04-16 23:24:39 +02:00
Michael SchättgenandGitHub 5d835b05ee Merge pull request #56 from ByteHamster/krop-color
Darkened Krop color
2019-04-16 22:26:55 +02:00
Michael Schättgen 2a47fa4de5 Add method to set global duration scale
Fixes #35
2019-04-16 22:12:49 +02:00
Alexander Bakker ead17949f9 Add simple example script to decrypt an Aegis vault 2019-04-16 20:21:15 +02:00
Alexander Bakker d5a848e79e Add a document detailing Aegis' security design and vault format 2019-04-16 20:21:15 +02:00
Alexander BakkerandGitHub 553cb16bc4 Merge pull request #57 from ByteHamster/arrays
Strings.xml cleanup
2019-04-14 18:03:16 +02:00
ByteHamster 47f8c9e648 Strings cleanup
- Removed unused strings
- Moved arrays to their own file, so translators do not have to deal with arrays
2019-04-14 17:18:14 +02:00
Alexander BakkerandGitHub 791138ccab Merge pull request #55 from ByteHamster/focus-password
Show keyboard automatically
2019-04-14 15:57:00 +02:00
Alexander BakkerandGitHub 6cbef27cfb Merge pull request #58 from ByteHamster/master
Added F-droid icon to readme
2019-04-14 15:56:21 +02:00
H. LehmannandGitHub e5f7dce9b8 Added F-droid icon 2019-04-14 14:24:53 +02:00
ByteHamster a415ef573e Show keyboard automatically 2019-04-14 14:15:34 +02:00
ByteHamster ffa2577cc2 Darkened Krop color 2019-04-14 00:47:21 +02:00
Alexander Bakker 2a0daae155 Fix crash that occurred when scheme of the URI in a QR code is missing 2019-04-10 11:42:35 +02:00
Michael Schättgen b41e328d22 Add ability to translate themes and viewmodes
Closes #50
2019-04-09 17:53:10 +02:00
Michael SchättgenandGitHub 1ac42d85d6 Merge pull request #53 from alexbakker/feature-autolock
Add an option to automatically lock the app
2019-04-09 17:34:47 +02:00
Michael SchättgenandGitHub 685a791aba Merge pull request #49 from zaggo0/master
Add Dutch translations
2019-04-09 17:34:35 +02:00
Michael Schättgen 89a1663e28 Fix bug where entries didn't get sorted properly 2019-04-09 17:15:25 +02:00
Alexander Bakker 0563ac917a Fix some rare crashes when restoring the app after termination
Also fixes an issue where the app shortcuts would not work for unencrypted vaults
2019-04-08 23:13:11 +02:00
Patrick van der Linden 719ef3bc71 Add Dutch translations
+ Fix preference groups not being translated
2019-04-08 15:00:24 +02:00
Alexander Bakker 18fd88a441 Add an option to automatically lock the app
This adds an option to automatically lock the app when:
* The back button is pressed
* The device is locked

It's the first step towards implementing #7
2019-04-07 18:36:13 +02:00
Alexander Bakker 6d93b78f9a Fix a crash on KitKat when clearing the KeyStore
Apparently KitKat doesn't like KeyPermanentlyInvalidatedException
2019-04-04 18:49:57 +02:00
Alexander Bakker 4365a693f2 Get Cipher back from FingerprintManager,CryptoObject after auth 2019-04-04 18:18:34 +02:00
Alexander Bakker 14fa493ac9 Remove keys from the KeyStore when disabling encryption or fingerprint 2019-04-04 15:27:24 +02:00
Alexander Bakker 36e3dd559c Normalize formatting and 'optimize' imports 2019-04-04 14:07:36 +02:00
Alexander Bakker b681d57b4e Update testdata file to use 'test' as the password 2019-04-03 15:03:56 +02:00
Alexander Bakker c5fa40166f Add some info about verifying APK's obtained from GitHub to the README 2019-04-02 21:37:49 +02:00
Michael Schättgen b7cf234db6 Add smaller view mode
Closes #39
2019-04-02 15:47:07 +02:00
86 changed files with 3128 additions and 1358 deletions
+59 -12
View File
@@ -2,13 +2,15 @@
Aegis is a free, secure and open source 2FA app for Android.
Aegis' security design and vault format is described in detail in [this document](docs/vault.md).
# Features
- Free and open source
- Secure
- Encryption (AES-256)
- Password (scrypt)
- Fingerprint (Android Keystore)
- Fingerprint (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
@@ -20,9 +22,12 @@ Aegis is a free, secure and open source 2FA app for Android.
- Aegis
- Import from apps (requires root):
- Google Authenticator
- Steam
- Supported algorithms:
- HOTP ([RFC 4226](https://tools.ietf.org/html/rfc4226))
- TOTP ([RFC 6238](https://tools.ietf.org/html/rfc6238))
- Steam ([RFC 6238](https://tools.ietf.org/html/rfc6238) with custom
encoding)
- Compatible with Google Authenticator
- Organization
- Custom or default icons
@@ -37,36 +42,78 @@ Aegis is a free, secure and open source 2FA app for Android.
## 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)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true)
[<img width=200 alt="Main Activity" src="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)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity"
src="metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main_dark.png?raw=true)
## Downloads
Aegis is available in the Google Play Store.
Aegis is available on the Google Play Store and on F-Droid.
[<img height=80 alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" />](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
[<img height=80 alt="Get it on Google Play"
src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
/>](http://play.google.com/store/apps/details?id=com.beemdevelopment.aegis)
[<img height="80" alt="Get it on F-Droid"
src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
/>](https://f-droid.org/app/com.beemdevelopment.aegis)
### Verification
There are plans to make it available on F-Droid as well.
APK releases on Google Play and GitHub are signed using the same key. They can
be verified using
[apksigner](https://developer.android.com/studio/command-line/apksigner.html#options-verify):
```
apksigner verify --print-certs --verbose aegis.apk
```
The output should look like:
```
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
```
The certificate fingerprints should correspond to the ones listed below:
```
Owner: CN=Beem Development
Issuer: CN=Beem Development
Serial number: 172380c
Valid from: Sat Feb 09 14:05:49 CET 2019 until: Wed Feb 03 14:05:49 CET 2044
Certificate fingerprints:
MD5: AA:EE:86:DB:C7:B8:88:9F:1F:C9:D0:7A:EC:37:36:32
SHA1: 59:FB:63:B7:1F:CE:95:74:6C:EB:1E:1A:CB:2C:2E:45:E5:FF:13:50
SHA256: C6:DB:80:A8:E1:4E:52:30:C1:DE:84:15:EF:82:0D:13:DC:90:1D:8F:E3:3C:F3:AC:B5:7B:68:62:D8:58:A8:23
```
## Libraries
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
- [FloatingActionButton](https://github.com/Clans/FloatingActionButton) by Dmytro Tarianyk
- [FloatingActionButton](https://github.com/Clans/FloatingActionButton) by
Dmytro Tarianyk
- [AppIntro](https://github.com/AppIntro/AppIntro) by Paolo Rotolo
- [Krop](https://github.com/avito-tech/krop) by Avito Technology
- [SpongyCastle](https://github.com/rtyley/spongycastle) by Roberto Tyley
- [Swirl](https://github.com/mattprecious/swirl) by Matthew Precious
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning Dodenhof
- [CircleImageView](https://github.com/hdodenhof/CircleImageView) by Henning
Dodenhof
- [barcodescanner](https://github.com/dm77/barcodescanner) by Dushyanth
- [libsu](https://github.com/topjohnwu/libsu) by John Wu
## License
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
This project is licensed under the GNU General Public License v3.0. See the
[LICENSE](LICENSE) file for details.
## Attribution
+3 -3
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 6
versionName "0.3"
versionCode 10
versionName "0.4"
}
buildTypes {
debug {
@@ -41,7 +41,7 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:v4.2.2'
implementation 'com.github.avito-tech:krop:3e65e12'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
+28 -38
View File
@@ -3,61 +3,51 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="com.beemdevelopment.aegis.AegisApplication"
android:name=".AegisApplication"
android:allowBackup="true"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
tools:replace="android:theme"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity
android:name="com.beemdevelopment.aegis.ui.MainActivity"
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}"
android:alwaysRetainTaskState="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.beemdevelopment.aegis.ui.ScannerActivity"
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="Scan a QR code"
android:theme="@style/AppTheme.Fullscreen"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>
android:screenOrientation="portrait" />
<activity
android:name="com.beemdevelopment.aegis.ui.EditEntryActivity"
android:label="Edit profile"
android:theme="@style/AppTheme.NoActionBar">
</activity>
android:name=".ui.EditEntryActivity"
android:label="Edit profile" />
<activity
android:name="com.beemdevelopment.aegis.ui.IntroActivity"
android:theme="@style/Theme.Intro"
android:launchMode="singleTop">
</activity>
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
<activity
android:name="com.beemdevelopment.aegis.ui.AuthActivity"
android:launchMode="singleTop">
</activity>
<activity android:name="com.beemdevelopment.aegis.ui.PreferencesActivity">
</activity>
android:name=".ui.SlotManagerActivity"
android:label="Manage key slots" />
<activity
android:name="com.beemdevelopment.aegis.ui.SlotManagerActivity"
android:label="Manage key slots"
android:theme="@style/AppTheme.TransparentActionBar">
</activity>
<activity
android:name="com.beemdevelopment.aegis.ui.GroupManagerActivity"
android:label="Manage groups"
android:theme="@style/AppTheme.TransparentActionBar">
</activity>
android:name=".ui.GroupManagerActivity"
android:label="Manage groups" />
</application>
</manifest>
</manifest>
@@ -1,7 +1,10 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
@@ -10,19 +13,27 @@ import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.ui.MainActivity;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.RequiresApi;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
@@ -37,6 +48,25 @@ public class AegisApplication extends Application {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
}
public void registerLockListener(LockListener listener) {
_lockListeners.add(listener);
}
public void unregisterLockListener(LockListener listener) {
_lockListeners.remove(listener);
}
public void lock() {
_manager.lock();
for (LockListener listener : _lockListeners) {
listener.onLocked();
}
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
@@ -57,4 +87,17 @@ public class AegisApplication extends Application {
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
}
}
}
public interface LockListener {
void onLocked();
}
}
@@ -11,10 +11,6 @@ public class Preferences {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public boolean isDarkModeEnabled() {
return _prefs.getBoolean("pref_dark_mode", false);
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
@@ -31,6 +27,10 @@ public class Preferences {
return _prefs.getBoolean("pref_intro", false);
}
public boolean isAutoLockEnabled() {
return _prefs.getBoolean("pref_auto_lock", true);
}
public void setIntroDone(boolean done) {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
@@ -43,35 +43,30 @@ public class Preferences {
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
}
public int getCurrentSortCategory() {
return _prefs.getInt("pref_current_sort_category", 0);
public SortCategory getCurrentSortCategory() {
return SortCategory.fromInteger(_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 Theme getCurrentTheme() {
return Theme.fromInteger(_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 ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_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);
}
@@ -1,71 +1,62 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import java.util.Collections;
import java.util.Comparator;
public enum SortCategory {
CUSTOM,
ACCOUNT,
ACCOUNTREVERSED,
ACCOUNT_REVERSED,
ISSUER,
ISSUERREVERSED;
ISSUER_REVERSED;
private static SortCategory[] _values;
static {
_values = values();
}
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;
return _values[x];
}
public static Comparator getComparator(SortCategory sortCategory) {
switch(sortCategory) {
public Comparator<DatabaseEntry> getComparator() {
Comparator<DatabaseEntry> comparator = null;
switch (this) {
case ACCOUNT:
case ACCOUNTREVERSED:
return new AccountNameComparator();
comparator = new AccountNameComparator();
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator());
break;
case ISSUER:
case ISSUERREVERSED:
return new IssuerNameComparator();
case CUSTOM:
return new IssuerNameComparator();
comparator = new IssuerNameComparator();
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
break;
}
return null;
return comparator;
}
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) {
public int getMenuItem() {
switch (this) {
case CUSTOM:
return R.id.menu_sort_custom;
case ACCOUNT:
return R.id.menu_sort_alphabetically_name;
case ACCOUNTREVERSED:
case ACCOUNT_REVERSED:
return R.id.menu_sort_alphabetically_name_reverse;
case ISSUER:
return R.id.menu_sort_alphabetically;
case ISSUERREVERSED:
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
default:
return R.id.menu_sort_custom;
}
@@ -5,35 +5,13 @@ public enum Theme {
DARK,
AMOLED;
private static Theme[] _values;
static {
_values = values();
}
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"
};
return _values[x];
}
}
@@ -1,33 +1,33 @@
package com.beemdevelopment.aegis;
import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT;
COMPACT,
SMALL;
private static ViewMode[] _values;
static {
_values = values();
}
public static ViewMode fromInteger(int x) {
switch(x) {
case 0:
return NORMAL;
case 1:
return COMPACT;
}
return null;
return _values[x];
}
public static String getViewModeName(int x) {
switch(x) {
case 0:
return "Normal";
case 1:
return "Compact";
@LayoutRes
public int getLayoutId() {
switch (this) {
case NORMAL:
return R.layout.card_entry;
case COMPACT:
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
default:
return R.layout.card_entry;
}
return null;
}
public static String[] getViewModeNames() {
return new String[] {
"Normal",
"Compact"
};
}
}
@@ -2,6 +2,8 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import org.spongycastle.crypto.generators.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -24,21 +26,24 @@ import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.spongycastle.crypto.generators.SCrypt;
public class CryptoUtils {
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
public static final int CRYPTO_SCRYPT_N = 1 << 15;
public static final int CRYPTO_SCRYPT_r = 8;
public static final int CRYPTO_SCRYPT_p = 1;
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
byte[] bytes = toBytes(password);
byte[] keyBytes = SCrypt.generate(bytes, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
return deriveKey(bytes, params);
}
public static Cipher createEncryptCipher(SecretKey key)
@@ -88,9 +93,14 @@ public class CryptoUtils {
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
return decrypt(encrypted, 0, encrypted.length, cipher, params);
}
public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
// append the tag to the ciphertext
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(encrypted);
stream.write(encrypted, encryptedOffset, encryptedLen);
stream.write(params.getTag());
encrypted = stream.toByteArray();
@@ -120,9 +130,11 @@ public class CryptoUtils {
return data;
}
private static byte[] toBytes(char[] chars) {
public static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);
return bytes;
}
}
@@ -14,12 +14,15 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import androidx.annotation.RequiresApi;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String STORE_NAME = "AndroidKeyStore";
@@ -76,22 +79,33 @@ public class KeyStoreHandle {
throw new KeyStoreHandleException(e);
}
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
if (isSupported()) {
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
return null;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new RuntimeException(e);
}
if (isSupported() && isKeyPermanentlyInvalidated(key)) {
return null;
}
return key;
}
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
// so instead, check for it here
if (e instanceof KeyPermanentlyInvalidatedException) {
return true;
}
throw new RuntimeException(e);
}
return false;
}
public void deleteKey(String id) throws KeyStoreHandleException {
try {
_keyStore.deleteEntry(id);
@@ -100,6 +114,16 @@ public class KeyStoreHandle {
}
}
public void clear() throws KeyStoreHandleException {
try {
for (String alias : Collections.list(_keyStore.aliases())) {
deleteKey(alias);
}
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.crypto.otp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@@ -1,7 +1,5 @@
package com.beemdevelopment.aegis.db;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
@@ -9,6 +7,8 @@ import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import androidx.annotation.NonNull;
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@@ -4,11 +4,10 @@ import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.crypto.MasterKeyException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import java.io.Serializable;
import com.beemdevelopment.aegis.db.slots.SlotList;
public class DatabaseFileCredentials implements Serializable {
private MasterKey _key;
private SlotList _slots;
@@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.db;
import android.content.Context;
import android.os.Environment;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import org.json.JSONObject;
import java.io.DataInputStream;
@@ -15,9 +18,6 @@ import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
public class DatabaseManager {
private static final String FILENAME = "aegis.json";
private static final String FILENAME_EXPORT = "aegis_export.json";
@@ -0,0 +1,49 @@
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
}
}
@@ -22,12 +22,12 @@ package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import androidx.annotation.RequiresApi;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.mattprecious.swirl.SwirlView;
import com.beemdevelopment.aegis.R;
import androidx.annotation.RequiresApi;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
@@ -3,12 +3,13 @@ package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionHelper {
private PermissionHelper() {
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@@ -1,12 +1,13 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.annotation.ArrayRes;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
import androidx.annotation.ArrayRes;
public class SpinnerHelper {
private SpinnerHelper() {
@@ -1,7 +1,6 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.views.EntryHolder;
import java.util.Comparator;
@@ -10,4 +9,4 @@ public class IssuerNameComparator implements Comparator<DatabaseEntry> {
public int compare(DatabaseEntry a, DatabaseEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
}
@@ -1,84 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AegisFileImporter extends DatabaseFileImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
JSONObject entryObj = array.getJSONObject(i);
try {
DatabaseEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return DatabaseEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -0,0 +1,109 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
public class AegisImporter extends DatabaseImporter {
public AegisImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
public State read(FileReader reader) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
DatabaseFile file = DatabaseFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (DatabaseFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private DatabaseFile _file;
private EncryptedState(DatabaseFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(DatabaseFileCredentials creds) throws DatabaseFileException {
JSONObject obj = _file.getContent(creds);
return new DecryptedState(obj);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
}
}
public static class DecryptedState extends State {
private JSONObject _obj;
private DecryptedState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
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 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());
}
}
}
}
@@ -1,99 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class AndOtpFileImporter extends DatabaseFileImporter {
private JSONArray _obj;
public AndOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
@Override
public void parse() throws DatabaseImporterException {
try {
_obj = new JSONArray(new String(_stream.getBytes(), StandardCharsets.UTF_8));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() throws DatabaseImporterException {
DatabaseImporterResult result = new DatabaseImporterResult();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -0,0 +1,191 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
public State read(FileReader reader) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = reader.readAll();
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return read(bytes);
} catch (JSONException e) {
// andOTP doesn't have a proper way to indicate whether a file is encrypted
// so, if we can't parse it as JSON, we'll have to assume it is
return new EncryptedState(bytes);
}
}
private static DecryptedState read(byte[] bytes) throws JSONException {
JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
return new DecryptedState(array);
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support encrypted andOTP backups
// it is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
// extract nonce and tag
byte[] nonce = Arrays.copyOfRange(_data, 0, CryptoUtils.CRYPTO_AEAD_NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - CryptoUtils.CRYPTO_AEAD_TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int offset = CryptoUtils.CRYPTO_AEAD_NONCE_SIZE;
int len = _data.length - CryptoUtils.CRYPTO_AEAD_NONCE_SIZE - CryptoUtils.CRYPTO_AEAD_TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _obj;
private DecryptedState(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
DatabaseEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static DatabaseEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
return new DatabaseEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -1,51 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class DatabaseAppImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseAppImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseAppImporter>> importers = new LinkedHashMap<>();
importers.put("Google Authenticator", GoogleAuthAppImporter.class);
importers.put("Steam", SteamAppImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected DatabaseAppImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseAppImporter create(Context context, Class<? extends DatabaseAppImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseAppImporter>> getImporters() {
return _importers;
}
}
@@ -1,55 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
public abstract class DatabaseFileImporter implements DatabaseImporter {
private static Map<String, Class<? extends DatabaseFileImporter>> _importers;
static {
// note: keep this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseFileImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisFileImporter.class);
importers.put("andOTP", AndOtpFileImporter.class);
importers.put("FreeOTP", FreeOtpFileImporter.class);
_importers = Collections.unmodifiableMap(importers);
}
private Context _context;
protected ByteInputStream _stream;
protected DatabaseFileImporter(Context context, ByteInputStream stream) {
_context = context;
_stream = stream;
}
public abstract void parse() throws DatabaseImporterException;
public abstract DatabaseImporterResult convert() throws DatabaseImporterException;
public abstract boolean isEncrypted();
public Context getContext() {
return _context;
}
public static DatabaseFileImporter create(Context context, ByteInputStream stream, Class<? extends DatabaseFileImporter> type) {
try {
return type.getConstructor(Context.class, ByteInputStream.class).newInstance(context, stream);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseFileImporter>> getImporters() {
return _importers;
}
}
@@ -1,14 +1,178 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public interface DatabaseImporter {
void parse() throws DatabaseImporterException;
DatabaseImporterResult convert() throws DatabaseImporterException;
boolean isEncrypted();
Context getContext();
public abstract class DatabaseImporter {
private Context _context;
private static Map<String, Class<? extends DatabaseImporter>> _importers;
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
static {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
}
public DatabaseImporter(Context context) {
_context = context;
}
protected Context getContext() {
return _context;
}
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(getAppPkgName(), getAppSubPath());
}
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
protected abstract String getAppPkgName();
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
public abstract State read(FileReader reader) throws DatabaseImporterException;
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return Collections.unmodifiableMap(_importers);
}
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
return Collections.unmodifiableMap(_appImporters);
}
public static abstract class State {
private boolean _encrypted;
public State(boolean encrypted) {
_encrypted = encrypted;
}
public boolean isEncrypted() {
return _encrypted;
}
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
if (!_encrypted) {
throw new RuntimeException("Attempted to decrypt a plain text database");
}
throw new UnsupportedOperationException();
}
public Result convert() throws DatabaseImporterException {
if (_encrypted) {
throw new RuntimeException("Attempted to convert database before decrypting it");
}
throw new UnsupportedOperationException();
}
}
public static class Result {
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;
}
}
public static class FileReader implements Closeable {
private InputStream _stream;
private FileReader(InputStream stream) {
_stream = stream;
}
public static FileReader open(String filename)
throws FileNotFoundException {
FileInputStream stream = new FileInputStream(filename);
return new FileReader(stream);
}
public static FileReader open(SuFile file)
throws FileNotFoundException {
SuFileInputStream stream = new SuFileInputStream(file);
return new FileReader(stream);
}
public static FileReader open(Context context, Uri uri)
throws FileNotFoundException {
InputStream stream = context.getContentResolver().openInputStream(uri);
return new FileReader(stream);
}
public byte[] readAll() throws IOException {
ByteInputStream stream = ByteInputStream.create(_stream);
return stream.getBytes();
}
public InputStream getStream() {
return _stream;
}
@Override
public void close() throws IOException {
_stream.close();
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
}
}
@@ -1,27 +0,0 @@
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;
}
}
@@ -3,6 +3,12 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -13,88 +19,95 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public class FreeOtpFileImporter extends DatabaseFileImporter {
private List<XmlEntry> _xmlEntries;
public FreeOtpFileImporter(Context context, ByteInputStream stream) {
super(context, stream);
}
private static class XmlEntry {
String Name;
String Value;
public FreeOtpImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
public State read(FileReader reader) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.setInput(reader.getStream(), null);
parser.nextTag();
_xmlEntries = parse(parser);
List<XmlEntry> entries = parse(parser);
return new State(entries);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
public static class State extends DatabaseImporter.State {
private List<XmlEntry> _entries;
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);
private State(List<XmlEntry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (XmlEntry xmlEntry : _entries) {
// TODO: order
if (!xmlEntry.Name.equals("tokenOrder")) {
try {
DatabaseEntry entry = convertEntry(xmlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
return result;
}
return result;
}
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(entry.Value);
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"));
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
switch (type) {
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
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, entry.Value);
}
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
public boolean isEncrypted() {
return false;
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
@@ -163,4 +176,9 @@ public class FreeOtpFileImporter extends DatabaseFileImporter {
}
}
}
private static class XmlEntry {
String Name;
String Value;
}
}
@@ -1,6 +1,5 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@@ -14,8 +13,6 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
@@ -25,30 +22,35 @@ import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthAppImporter extends DatabaseAppImporter {
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@SuppressLint("SdCardPath")
private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases";
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
private List<Entry> _entries = new ArrayList<>();
public GoogleAuthAppImporter(Context context) {
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
public State read(FileReader reader) throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (SuFileInputStream in = new SuFileInputStream(new SuFile(_filename))) {
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(in, out);
}
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(reader.getStream(), out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
@@ -56,14 +58,16 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
if (!cursor.moveToFirst()) {
return;
List<Entry> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
Entry entry = new Entry(cursor);
entries.add(entry);
} while(cursor.moveToNext());
}
do {
Entry entry = new Entry(cursor);
_entries.add(entry);
} while(cursor.moveToNext());
return new State(entries);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
@@ -73,53 +77,57 @@ public class GoogleAuthAppImporter extends DatabaseAppImporter {
}
}
@Override
public DatabaseImporterResult convert() {
DatabaseImporterResult result = new DatabaseImporterResult();
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
for (Entry sqlEntry : _entries) {
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
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 {
DatabaseEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
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());
}
}
return result;
}
private static DatabaseEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new DatabaseEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
@Override
public boolean isEncrypted() {
return false;
}
private static String getString(Cursor cursor, String columnName) {
@@ -1,80 +0,0 @@
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;
}
}
@@ -0,0 +1,89 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
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 org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
public SteamImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
// NOTE: this assumes that a global root shell has already been obtained by the caller
SuFile path = getAppPath(getAppPkgName(), _subDir);
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
}
// TODO: handle multiple files (can this even occur?)
return new SuFile(_subDir, files[0].getName()).getPath();
}
public State read(FileReader reader) throws DatabaseImporterException {
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONObject _obj;
private State(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() {
Result result = new Result();
try {
DatabaseEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static 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());
}
}
}
}
@@ -57,7 +57,8 @@ public class GoogleAuthInfo {
}
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
if (!uri.getScheme().equals("otpauth")) {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("otpauth")) {
throw new GoogleAuthInfoException("unsupported protocol");
}
@@ -1,29 +1,65 @@
package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.provider.Settings;
import android.view.WindowManager;
import android.widget.Toast;
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 {
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private AegisApplication _app;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set the theme and create the activity
setPreferredTheme(getPreferences().getCurrentTheme());
super.onCreate(savedInstanceState);
// if the app was killed, relaunch MainActivity and close everything else
if (savedInstanceState != null && isOrphan()) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return;
}
// set FLAG_SECURE on the window of every AegisActivity
if (getPreferences().isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// set the theme
setPreferredTheme(Theme.fromInteger(getPreferences().getCurrentTheme()));
// apply a dirty hack to make progress bars work even if animations are disabled
setGlobalAnimationDurationScale();
// register a callback to listen for lock events
_app.registerLockListener(this);
}
@Override
protected void onDestroy() {
_app.unregisterLockListener(this);
super.onDestroy();
}
@CallSuper
@Override
public void onLocked() {
if (isOrphan()) {
setResult(RESULT_CANCELED, null);
finish();
}
}
protected AegisApplication getApp() {
@@ -39,14 +75,31 @@ public abstract class AegisActivity extends AppCompatActivity {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack);
break;
}
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was locked by an external trigger while the Activity was still open.
*/
private boolean isOrphan() {
return !(this instanceof MainActivity) && _app.getDatabaseManager().isLocked();
}
private void setGlobalAnimationDurationScale() {
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
if (durationScale != 1) {
try {
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
} catch (Throwable t) {
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
}
}
}
}
@@ -1,48 +1,48 @@
package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import androidx.appcompat.app.AlertDialog;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import androidx.appcompat.app.AlertDialog;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
private EditText _textPassword;
private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private Cipher _fingerCipher;
private FingerprintManager.CryptoObject _fingerCryptoObj;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -87,7 +87,8 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
invalidated = true;
continue;
}
_fingerCipher = slot.createDecryptCipher(key);
Cipher cipher = slot.createDecryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
invalidated = false;
@@ -95,7 +96,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
}
}
} catch (KeyStoreHandleException | SlotException e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
// display a help message if a matching invalidated keystore entry was found
@@ -107,10 +108,18 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
decryptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
});
if (_fingerHelper == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
}
private void showError() {
@@ -118,7 +127,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
}
@@ -135,6 +144,13 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
finish();
}
private void selectPassword() {
_textPassword.selectAll();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
@Override
public void onBackPressed() {
// ignore back button presses
@@ -145,7 +161,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
super.onResume();
if (_fingerHelper != null) {
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
_fingerHelper.startListening(_fingerCryptoObj);
}
}
@@ -160,7 +176,7 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
@Override
public void onAuthenticated() {
trySlots(FingerprintSlot.class, _fingerCipher);
trySlots(FingerprintSlot.class, _fingerCryptoObj.getCipher());
}
@Override
@@ -6,7 +6,9 @@ import android.content.Context;
import android.content.DialogInterface;
import android.hardware.fingerprint.FingerprintManager;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
@@ -14,8 +16,14 @@ import android.widget.EditText;
import android.widget.NumberPicker;
import android.widget.TextView;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
@@ -30,14 +38,6 @@ import javax.crypto.SecretKey;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
public class Dialogs {
private Dialogs() {
@@ -54,17 +54,6 @@ public class Dialogs {
dialog.show();
}
public static void showTextInputDialog(Context context, @StringRes int titleId, TextInputListener listener) {
EditText input = new EditText(context);
showSecureDialog(new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(input)
.setPositiveButton(android.R.string.ok, (dialog, which) ->
listener.onTextInputResult(input.getText().toString()))
.create());
}
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
@@ -130,8 +119,12 @@ public class Dialogs {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) { }
public void afterTextChanged(Editable c) { }
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
}
public void afterTextChanged(Editable c) {
}
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
@@ -139,20 +132,34 @@ public class Dialogs {
showSecureDialog(dialog);
}
public static void showCreateNewGroupDialog(Activity activity, TextInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_newgroup, null);
EditText groupName = view.findViewById(R.id.text_groupname);
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
if (isSecret) {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
input.setHint(hintId);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_group)
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onTextInputResult(groupName.getText().toString()))
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
})
.create();
showSecureDialog(dialog);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
@@ -176,7 +183,7 @@ public class Dialogs {
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
Cipher cipher;
FingerprintManager.CryptoObject obj;
FingerprintSlot slot;
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
FingerprintManager manager = FingerprintHelper.getManager(activity);
@@ -184,7 +191,8 @@ public class Dialogs {
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
Cipher cipher = Slot.createEncryptCipher(key);
obj = new FingerprintManager.CryptoObject(cipher);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
}
@@ -201,7 +209,7 @@ public class Dialogs {
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, cipher);
listener.onSlotResult(slot, obj.getCipher());
dialog.dismiss();
}
@@ -211,7 +219,7 @@ public class Dialogs {
}
}));
helper.get().startListening(new FingerprintManager.CryptoObject(cipher));
helper.get().startListening(obj);
showSecureDialog(dialog);
}
@@ -220,7 +228,7 @@ public class Dialogs {
}
public interface TextInputListener {
void onTextInputResult(String text);
void onTextInputResult(char[] text);
}
public interface SlotListener {
@@ -10,9 +10,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.ArrayRes;
import androidx.appcompat.app.ActionBar;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
@@ -30,6 +27,8 @@ import android.widget.TableRow;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@@ -54,10 +53,10 @@ import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.ArrayRes;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
@@ -217,15 +216,16 @@ public class EditEntryActivity extends AegisActivity {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position == _spinnerGroupList.size() - 1) {
Dialogs.showCreateNewGroupDialog(activity, text -> {
if (text.isEmpty()) {
Dialogs.showTextInputDialog(activity, R.string.set_group, R.string.group_name_hint, text -> {
String str = new String(text);
if (str.isEmpty()) {
return;
}
_groups.add(text);
_groups.add(str);
// reset the selection to "No group" to work around a quirk
_spinnerGroup.setSelection(0, false);
updateGroupSpinnerList();
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(text), false);
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(str), false);
});
_spinnerGroup.setSelection(prevPosition, false);
} else {
@@ -4,6 +4,9 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.TreeSet;
@@ -12,8 +15,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
@@ -2,35 +2,34 @@ package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import android.view.WindowManager;
import com.github.paolorotolo.appintro.AppIntro2;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.github.paolorotolo.appintro.AppIntro2;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.slots.SlotException;
import androidx.fragment.app.Fragment;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
@@ -1,48 +1,53 @@
package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
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.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
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;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
@@ -53,6 +58,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6;
private static final int CODE_SCAN_IMAGE = 7;
// permission request codes
private static final int CODE_PERM_CAMERA = 0;
@@ -61,12 +67,13 @@ 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;
private EntryListView _entryListView;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -83,7 +90,8 @@ 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()));
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
@@ -91,10 +99,21 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_fabMenu.collapse();
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
}
@Override
@@ -119,9 +138,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onNewIntent(intent);
setIntent(intent);
if (!doShortcutActions() || _db.isLocked()) {
unlockDatabase(null);
}
doShortcutActions();
}
@Override
@@ -130,6 +147,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && _db.isLocked()) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(resultCode, data);
@@ -152,6 +174,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
break;
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
}
}
@@ -177,13 +201,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
int viewMode = getPreferences().getCurrentViewMode();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
}
}
@@ -235,6 +258,38 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void onScanImageResult(int resultCode, Intent intent) {
if (resultCode == RESULT_OK) {
Uri inputFile = (intent.getData());
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new MultiFormatReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
DatabaseEntry entry = new DatabaseEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
}
private void updateGroupFilterMenu() {
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
for (int i = menu.size() - 1; i >= 0; i--) {
@@ -262,26 +317,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
private void updateSortCategoryMenu() {
SortCategory category = getPreferences().getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_checkedGroup = group;
_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);
_entryListView.setGroupFilter(group, true);
}
private void addEntry(DatabaseEntry entry) {
@@ -293,7 +337,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
// TODO: user feedback
Exception e = (Exception) data.getSerializableExtra("exception");
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
@@ -316,15 +360,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(scannerActivity, CODE_SCAN);
}
private boolean doShortcutActions() {
// return false if an action was blocked by a locked database
// otherwise, always return true
private void doShortcutActions() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null) {
return true;
} else if (_db.isLocked()) {
return false;
if (action == null || _db.isLocked()) {
return;
}
switch (action) {
@@ -334,12 +374,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
intent.removeExtra("action");
return true;
}
public void startActivityForResult(Intent intent, int requestCode) {
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
super.startActivityForResult(intent, requestCode);
}
@Override
@@ -366,12 +400,22 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// refresh all codes to prevent showing old ones
_entryListView.refresh(true);
_entryListView.refresh(false);
} else {
loadEntries();
}
updateLockIcon();
doShortcutActions();
}
@Override
public void onBackPressed() {
if (_app.isAutoLockEnabled()) {
_app.lock();
}
super.onBackPressed();
}
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
@@ -425,7 +469,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
updateSortCategoryMenu(SortCategory.fromInteger(getPreferences().getCurrentSortCategory()));
updateSortCategoryMenu();
}
return true;
}
@@ -438,7 +482,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(intent, CODE_PREFERENCES);
return true;
case R.id.action_lock:
lockDatabase();
_app.lock();
startAuthActivity();
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
@@ -459,41 +504,28 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case R.id.menu_sort_alphabetically:
sortCategory = SortCategory.ISSUER;
break;
case R.id.menu_sort_alphabetically_reverse:
sortCategory = SortCategory.ISSUERREVERSED;
sortCategory = SortCategory.ISSUER_REVERSED;
break;
case R.id.menu_sort_alphabetically_name:
sortCategory = SortCategory.ACCOUNT;
break;
case R.id.menu_sort_alphabetically_name_reverse:
sortCategory = SortCategory.ACCOUNTREVERSED;
sortCategory = SortCategory.ACCOUNT_REVERSED;
break;
case R.id.menu_sort_custom:
default:
sortCategory = SortCategory.CUSTOM;
break;
}
setSortCategory(sortCategory);
_entryListView.setSortCategory(sortCategory, true);
getPreferences().setCurrentSortCategory(sortCategory);
}
return super.onOptionsItemSelected(item);
}
}
private void lockDatabase() {
if (_loaded) {
_entryListView.clearEntries();
_db.lock();
_loaded = false;
startAuthActivity();
}
}
private void unlockDatabase(DatabaseFileCredentials creds) {
if (_loaded) {
return;
@@ -518,20 +550,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
loadEntries();
SortCategory currentSortCategory = SortCategory.fromInteger(getPreferences().getCurrentSortCategory());
setSortCategory(currentSortCategory);
}
private void loadEntries() {
// load all entries
List<DatabaseEntry> entries = new ArrayList<DatabaseEntry>(_db.getEntries());
_entryListView.addEntries(entries);
_entryListView.runEntriesAnimation();
_loaded = true;
}
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("slots", _db.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
startActivityForResult(intent, CODE_DECRYPT);
}
@@ -571,37 +603,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
saveDatabase();
}
private boolean isAnimating;
@Override
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !isAnimating) {
isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public void onLocked() {
_entryListView.clearEntries();
_loaded = false;
super.onLocked();
}
}
@@ -2,59 +2,26 @@ 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.content.pm.PackageManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Toast;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.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;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
@@ -62,7 +29,30 @@ import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.importers.AegisImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
@@ -70,6 +60,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private static final int CODE_IMPORT_DECRYPT = 1;
private static final int CODE_SLOTS = 2;
private static final int CODE_GROUPS = 3;
private static final int CODE_SELECT_ENTRIES = 4;
// permission request codes
private static final int CODE_PERM_IMPORT = 0;
@@ -78,10 +69,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private Intent _result;
private DatabaseManager _db;
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseFileImporter _importer;
private Class<? extends DatabaseFileImporter> _importerType;
// keep a reference to the type of database converter the user selected
private Class<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
@@ -99,18 +89,17 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// set the result intent in advance
setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme();
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary("Selected: " + Theme.getThemeName(currentTheme));
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
String[] themeNames = Theme.getThemeNames();
int checkedTheme = app.getPreferences().getCurrentTheme();
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_theme))
.setSingleChoiceItems(themeNames, checkedTheme, (dialog, which) -> {
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
@@ -126,34 +115,22 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
darkModePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
}
});
int currentViewMode = app.getPreferences().getCurrentViewMode();
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary("Selected: " + ViewMode.getViewModeName(currentViewMode));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
String[] viewModes = ViewMode.getViewModeNames();
int checkedMode = app.getPreferences().getCurrentViewMode();
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_view_mode))
.setSingleChoiceItems(viewModes, checkedMode, (dialog, which) -> {
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
_result.putExtra("needsRefresh", true);
dialog.dismiss();
viewModePreference.setSummary("Selected: " + ViewMode.getViewModeName(i));
_result.putExtra("needsRecreate", true);
})
.setPositiveButton(android.R.string.ok, null)
.create());
@@ -255,15 +232,25 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.disable_encryption))
.setTitle(R.string.disable_encryption)
.setMessage(getString(R.string.disable_encryption_description))
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
_db.disableEncryption();
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.encrypting_error), Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
return;
}
// clear the KeyStore
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.clear();
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
updateEncryptionPreferences();
}
})
@@ -289,6 +276,14 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
slots.remove(slot);
_db.setCredentials(creds);
// remove the KeyStore key
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.deleteKey(slot.getUUID().toString());
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
saveDatabase();
updateEncryptionPreferences();
}
@@ -324,13 +319,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return true;
}
});
}
@Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
updateEncryptionPreferences();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (!PermissionHelper.checkResults(grantResults)) {
Toast.makeText(getActivity(), getString(R.string.permission_denied), Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.permission_denied, Toast.LENGTH_SHORT).show();
return;
}
@@ -363,6 +363,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
}
}
@@ -380,11 +383,11 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
Map<String, Class<? extends DatabaseFileImporter>> importers = DatabaseFileImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
.setTitle(R.string.choose_application)
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
@@ -400,61 +403,95 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
private void onImportApp() {
Map<String, Class<? extends DatabaseAppImporter>> importers = DatabaseAppImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getAppImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(getString(R.string.choose_application))
.setTitle(R.string.choose_application)
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
try {
DatabaseAppImporter importer;
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importer = DatabaseAppImporter.create(getContext(), importers.get(names[i]));
importer.parse();
} catch (IOException e) {
Toast.makeText(getActivity(), getString(R.string.root_error), Toast.LENGTH_SHORT).show();
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Class<? extends DatabaseImporter> importerType = Objects.requireNonNull(importers.get(names[i]));
DatabaseImporter importer = DatabaseImporter.create(getContext(), importerType);
importApp(importer);
})
.create());
}
private void importApp(DatabaseImporter importer) {
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()
try (Shell shell = Shell.getShell()) {
if (!shell.isRoot()) {
Toast.makeText(getActivity(), R.string.root_error, Toast.LENGTH_SHORT).show();
return;
}
SuFile file = importer.getAppPath();
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(file)) {
importDatabase(importer, reader);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
} catch (IOException | DatabaseImporterException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
try {
DatabaseImporter.State state = importer.read(reader);
if (state.isEncrypted()) {
// temporary special case for encrypted Aegis databases
if (state instanceof AegisImporter.EncryptedState) {
_importerState = state;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
} else {
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
}
@Override
public void onError(Exception e) {
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
}
});
}
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importer = null;
_importerState = null;
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
((AegisFileImporter)_importer).setCredentials(creds);
DatabaseImporter.State state;
try {
importDatabase(_importer);
} catch (DatabaseImporterException e) {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (DatabaseFileException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
return;
}
_importer = null;
importDatabase(state);
_importerState = null;
}
private void onImportResult(int resultCode, Intent data) {
@@ -463,85 +500,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
ByteInputStream stream;
try (InputStream fileStream = getActivity().getContentResolver().openInputStream(uri)) {
stream = ByteInputStream.create(fileStream);
try (DatabaseImporter.FileReader reader = DatabaseImporter.FileReader.open(getContext(), uri)) {
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
importDatabase(importer, reader);
} catch (FileNotFoundException e) {
Toast.makeText(getActivity(), getString(R.string.file_not_found), Toast.LENGTH_SHORT).show();
return;
Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getActivity(), getString(R.string.reading_file_error), Toast.LENGTH_SHORT).show();
return;
}
try {
DatabaseFileImporter importer = DatabaseFileImporter.create(getContext(), stream, _importerType);
importer.parse();
// special case to decrypt encrypted aegis databases
if (importer.isEncrypted() && importer instanceof AegisFileImporter) {
_importer = importer;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisFileImporter)_importer).getFile().getHeader().getSlots());
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
return;
}
importDatabase(importer);
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter importer) throws DatabaseImporterException {
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) {
entry.resetUUID();
}
_db.addEntry(entry);
}
if (!saveDatabase()) {
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
return;
}
_result.putExtra("needsRecreate", true);
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();
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();
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) errors);
startActivityForResult(intent, CODE_SELECT_ENTRIES);
}
private void onExport() {
@@ -558,7 +545,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
try {
filename = _db.export(checked.get());
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.exporting_database_error), Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), R.string.exporting_database_error, Toast.LENGTH_SHORT).show();
return;
}
@@ -578,7 +565,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
} else {
builder.setMessage(getString(R.string.export_warning));
builder.setMessage(R.string.export_warning);
}
Dialogs.showSecureDialog(builder.create());
}
@@ -608,11 +595,36 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private void onSelectEntriesResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) data.getSerializableExtra("entries");
for (DatabaseEntry entry : entries) {
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
entry.resetUUID();
}
_db.addEntry(entry);
}
if (!saveDatabase()) {
return;
}
String toastMessage = getResources().getString(R.string.imported_entries_count, entries.size());
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
_result.putExtra("needsRecreate", true);
}
private boolean saveDatabase() {
try {
_db.save();
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.saving_error), Toast.LENGTH_LONG).show();
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
return false;
}
@@ -8,19 +8,19 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import java.util.Collections;
import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
@@ -0,0 +1,138 @@
package com.beemdevelopment.aegis.ui;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.getbase.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class SelectEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_select_entries);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
Intent intent = getIntent();
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
for (DatabaseEntry entry : entries) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
}
if (errors.size() > 0) {
showErrorDialog(errors);
}
FloatingActionButton fabMenu = findViewById(R.id.fab);
fabMenu.setOnClickListener(v -> returnSelectedEntries());
_fabScrollHelper = new FabScrollHelper(fabMenu);
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getString(R.string.import_error_dialog, errors.size()))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
.create());
}
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
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(this)
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
})
.create());
}
private void returnSelectedEntries() {
List<DatabaseEntry> entries = _adapter.getSelectedEntries();
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
public void onScroll(int dx, int dy) {
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_select_entries, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -2,30 +2,29 @@ package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
import javax.crypto.Cipher;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
@@ -0,0 +1,37 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class ImportEntry {
private DatabaseEntry _entry;
private boolean _isChecked = true;
private Listener _listener;
public ImportEntry(DatabaseEntry entry) {
_entry = entry;
}
public void setOnCheckedChangedListener(Listener listener) {
_listener = listener;
}
public DatabaseEntry getDatabaseEntry() {
return _entry;
}
public boolean isChecked() {
return _isChecked;
}
public void setIsChecked(boolean isChecked) {
_isChecked = isChecked;
if (_listener != null) {
_listener.onCheckedChanged(_isChecked);
}
}
public interface Listener {
void onCheckedChanged(boolean value);
}
}
@@ -2,10 +2,11 @@ package com.beemdevelopment.aegis.ui.preferences;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import androidx.annotation.RequiresApi;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import android.util.AttributeSet;
public class SwitchPreference extends SwitchPreferenceCompat {
private Preference.OnPreferenceChangeListener _listener;
@@ -5,8 +5,6 @@ import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -14,23 +12,22 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import androidx.fragment.app.Fragment;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
@@ -44,7 +41,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private boolean _fingerAuthenticated;
@Override
@@ -74,7 +71,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
public Cipher getFingerCipher() {
return _fingerCipher;
return _fingerCryptoObj.getCipher();
}
public FingerprintSlot getFingerSlot() {
@@ -105,7 +102,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
if (_fingerHelper == null) {
@@ -114,11 +111,12 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
try {
_fingerCipher = Slot.createEncryptCipher(key);
Cipher cipher = Slot.createEncryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
_fingerHelper.startListening(_fingerCryptoObj);
break;
default:
throw new RuntimeException();
@@ -3,8 +3,6 @@ package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -12,10 +10,12 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
@@ -2,15 +2,13 @@ package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
private Callback _cb;
@@ -4,9 +4,11 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Process;
import androidx.annotation.CallSuper;
import com.beemdevelopment.aegis.ui.Dialogs;
import androidx.annotation.CallSuper;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
private ProgressDialog _dialog;
@@ -2,19 +2,17 @@ package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.crypto.MasterKey;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
private Callback _cb;
@@ -1,14 +1,13 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
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.db.DatabaseEntry;
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;
@@ -16,11 +15,11 @@ import com.beemdevelopment.aegis.otp.TotpInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import androidx.recyclerview.widget.RecyclerView;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private List<DatabaseEntry> _entries;
@@ -31,8 +30,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private int _tapToRevealTime;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private boolean _isPeriodUniform = true;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
@@ -58,26 +57,43 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
if (isEntryFiltered(entry)) {
return;
}
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
boolean added = false;
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
// insert the entry in the correct order
// note: this assumes that _shownEntries has already been sorted
for (int i = 0; i < _shownEntries.size(); i++) {
if (comparator.compare(_shownEntries.get(i), entry) > 0) {
_shownEntries.add(i, entry);
notifyItemInserted(i);
added = true;
break;
}
}
}
if (!added){
_shownEntries.add(entry);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
checkPeriodUniformity();
}
public void addEntries(List<DatabaseEntry> entries) {
_entries.addAll(entries);
for (DatabaseEntry entry : entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
notifyDataSetChanged();
updateShownEntries();
checkPeriodUniformity();
}
public void removeEntry(DatabaseEntry entry) {
@@ -89,6 +105,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_shownEntries.remove(position);
notifyItemRemoved(position);
}
checkPeriodUniformity();
}
public void clearEntries() {
@@ -117,6 +135,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
int position = getItemCount() - 1;
notifyItemInserted(position);
}
checkPeriodUniformity();
}
private boolean isEntryFiltered(DatabaseEntry entry) {
@@ -146,30 +166,45 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
}
public void setGroupFilter(String group) {
public void setGroupFilter(String group, boolean apply) {
if (_groupFilter != null && _groupFilter.equals(group)) {
return;
}
_groupFilter = group;
if (apply) {
updateShownEntries();
checkPeriodUniformity();
}
}
public void setSortCategory(SortCategory category, boolean apply) {
if (_sortCategory == category) {
return;
}
_sortCategory = category;
if (apply) {
updateShownEntries();
}
}
private void updateShownEntries() {
// clear the list of shown entries first
_shownEntries.clear();
// add entries back that are not filtered out
for (DatabaseEntry entry : _entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
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();
// sort the remaining list of entries
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
Collections.sort(_shownEntries, comparator);
}
_sortCategory = sortCategory;
notifyDataSetChanged();
}
public void setViewMode(ViewMode viewMode) {
@@ -208,14 +243,14 @@ 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_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);
}
public int getItemViewType(int position) {
return _viewMode.getLayoutId();
}
@Override
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
return new EntryHolder(view);
}
@@ -231,9 +266,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress, _tapToReveal);
holder.setTapToRevealTime(_tapToRevealTime);
if (showProgress) {
holder.startRefreshLoop();
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@@ -275,6 +307,20 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_holders.add(holder);
}
private void checkPeriodUniformity() {
boolean uniform = isPeriodUniform();
if (uniform == _isPeriodUniform) {
return;
}
_isPeriodUniform = uniform;
for (EntryHolder holder : _holders) {
holder.setShowProgress(!_isPeriodUniform);
}
_listener.onPeriodUniformityChanged(_isPeriodUniform);
}
public int getUniformPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (DatabaseEntry entry : _shownEntries) {
@@ -313,5 +359,6 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onPeriodUniformityChanged(boolean uniform);
}
}
@@ -3,13 +3,14 @@ package com.beemdevelopment.aegis.ui.views;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PorterDuff;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Handler;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.HotpInfo;
@@ -17,9 +18,7 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import android.os.Handler;
import androidx.recyclerview.widget.RecyclerView;
public class EntryHolder extends RecyclerView.ViewHolder {
private TextView _profileName;
@@ -28,11 +27,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private View _entryDivider;
private View _currentView;
private boolean _hidden;
private int _tapToRevealTime;
@@ -43,7 +39,6 @@ public class EntryHolder extends RecyclerView.ViewHolder {
public EntryHolder(final View view) {
super(view);
_currentView = view;
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
@@ -81,14 +76,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_hidden = hidden;
// only show the progress bar if there is no uniform period and the entry type is TotpInfo
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
if (showProgress) {
_progressBar.setPeriod(((TotpInfo)entry.getInfo()).getPeriod());
if (_entryDivider != null) {
_entryDivider.setVisibility(View.GONE);
}
}
setShowProgress(showProgress);
// only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
@@ -126,6 +114,25 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_buttonRefresh.setOnClickListener(listener);
}
public void setShowProgress(boolean showProgress) {
if (_entry.getInfo() instanceof HotpInfo) {
showProgress = false;
}
_progressBar.setVisibility(showProgress ? View.VISIBLE : View.GONE);
if (showProgress) {
_progressBar.setPeriod(((TotpInfo) _entry.getInfo()).getPeriod());
if (_entryDivider != null) {
_entryDivider.setVisibility(View.GONE);
}
startRefreshLoop();
} else {
stopRefreshLoop();
}
}
public void startRefreshLoop() {
_refresher.start();
}
@@ -163,7 +170,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
private void hideCode() {
_profileCode.setText(_currentView.getContext().getResources().getString(R.string.tap_to_reveal));
_profileCode.setText(R.string.tap_to_reveal);
_hidden = true;
}
@@ -2,34 +2,33 @@ package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import java.util.List;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private RecyclerView _rvKeyProfiles;
private RecyclerView _recyclerView;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
@@ -45,12 +44,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_entry_list_view, container, false);
_progressBar = view.findViewById(R.id.progressBar);
// set up the recycler view
_rvKeyProfiles = view.findViewById(R.id.rvKeyProfiles);
_rvKeyProfiles.addOnScrollListener(new RecyclerView.OnScrollListener() {
_recyclerView = view.findViewById(R.id.rvKeyProfiles);
_recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
@@ -59,15 +57,15 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
});
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_rvKeyProfiles.setLayoutManager(mLayoutManager);
_recyclerView.setLayoutManager(mLayoutManager);
_touchCallback = new SimpleItemTouchHelperCallback(_adapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(_touchCallback);
touchHelper.attachToRecyclerView(_rvKeyProfiles);
_rvKeyProfiles.setAdapter(_adapter);
touchHelper.attachToRecyclerView(_recyclerView);
_recyclerView.setAdapter(_adapter);
int resId = R.anim.layout_animation_fall_down;
LayoutAnimationController animation = AnimationUtils.loadLayoutAnimation(getContext(), resId);
_rvKeyProfiles.setLayoutAnimation(animation);
_recyclerView.setLayoutAnimation(animation);
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
@@ -84,23 +82,26 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
return view;
}
public void setGroupFilter(String group) {
_adapter.setGroupFilter(group);
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
checkPeriodUniformity();
_adapter.setGroupFilter(group, apply);
runLayoutAnimation(_rvKeyProfiles);
if (apply) {
runEntriesAnimation();
}
}
public void setSortCategory(SortCategory sortCategory) {
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
_adapter.setSortCategory(sortCategory);
runLayoutAnimation(_rvKeyProfiles);
if (apply) {
runEntriesAnimation();
}
}
public void setViewMode(ViewMode viewMode) {
_adapter.setViewMode(viewMode);
public void setViewMode(ViewMode mode) {
_adapter.setViewMode(mode);
}
public void refresh(boolean hard) {
@@ -110,33 +111,6 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_adapter.refresh(hard);
}
private void checkPeriodUniformity() {
boolean uniform = _adapter.isPeriodUniform();
if (uniform == _showProgress) {
return;
}
_showProgress = uniform;
if (_showProgress) {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
startRefreshLoop();
} else {
_progressBar.setVisibility(View.GONE);
stopRefreshLoop();
}
}
private void startRefreshLoop() {
refresh(true);
_refresher.start();
}
private void stopRefreshLoop() {
refresh(true);
_refresher.stop();
}
public void setListener(Listener listener) {
_listener = listener;
}
@@ -166,6 +140,19 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_listener.onEntryChange(entry);
}
@Override
public void onPeriodUniformityChanged(boolean isUniform) {
_showProgress = isUniform;
if (_showProgress) {
_progressBar.setVisibility(View.VISIBLE);
_progressBar.setPeriod(_adapter.getUniformPeriod());
_refresher.start();
} else {
_progressBar.setVisibility(View.GONE);
_refresher.stop();
}
}
public void setShowAccountName(boolean showAccountName) {
_adapter.setShowAccountName(showAccountName);
}
@@ -180,37 +167,31 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
public void addEntry(DatabaseEntry entry) {
_adapter.addEntry(entry);
checkPeriodUniformity();
}
public void addEntries(List<DatabaseEntry> entries) {
_adapter.addEntries(entries);
checkPeriodUniformity();
}
public void removeEntry(DatabaseEntry entry) {
_adapter.removeEntry(entry);
checkPeriodUniformity();
}
public void clearEntries() {
_adapter.clearEntries();
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry entry) {
_adapter.replaceEntry(entry);
checkPeriodUniformity();
}
private void runLayoutAnimation(final RecyclerView recyclerView) {
final Context context = recyclerView.getContext();
public void runEntriesAnimation() {
final Context context = _recyclerView.getContext();
final LayoutAnimationController controller =
AnimationUtils.loadLayoutAnimation(context, R.anim.layout_animation_fall_down);
recyclerView.setLayoutAnimation(controller);
recyclerView.getAdapter().notifyDataSetChanged();
recyclerView.scheduleLayoutAnimation();
_recyclerView.setLayoutAnimation(controller);
_recyclerView.scheduleLayoutAnimation();
}
public interface Listener {
@@ -219,6 +200,5 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onScroll(int dx, int dy);
}
}
@@ -4,10 +4,11 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import java.util.ArrayList;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
public class GroupAdapter extends RecyclerView.Adapter<GroupHolder> {
private GroupAdapter.Listener _listener;
@@ -1,12 +1,13 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import androidx.recyclerview.widget.RecyclerView;
public class GroupHolder extends RecyclerView.ViewHolder {
private TextView _slotName;
private ImageView _buttonDelete;
@@ -0,0 +1,97 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder> {
private List<ImportEntry> _entries;
public ImportEntriesAdapter() {
_entries = new ArrayList<>();
}
public void addEntry(ImportEntry entry) {
_entries.add(entry);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
@NonNull
@Override
public ImportEntryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_import_entry, parent, false);
return new ImportEntryHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ImportEntryHolder holder, int position) {
ImportEntry entry = _entries.get(position);
entry.setOnCheckedChangedListener(holder);
holder.setData(entry);
}
@Override
public void onViewRecycled(@NonNull ImportEntryHolder holder) {
holder.getEntry().setOnCheckedChangedListener(null);
}
@Override
public int getItemCount() {
return _entries.size();
}
public List<DatabaseEntry> getSelectedEntries() {
List<DatabaseEntry> entries = new ArrayList<>();
for (ImportEntry entry : getCheckedEntries()) {
entries.add(entry.getDatabaseEntry());
}
return entries;
}
private List<ImportEntry> getCheckedEntries() {
List<ImportEntry> entries = new ArrayList<>();
for (ImportEntry entry : _entries) {
if (entry.isChecked()) {
entries.add(entry);
}
}
return entries;
}
public void toggleCheckboxes() {
int checkedEntries = getCheckedEntries().size();
if (checkedEntries == 0 || checkedEntries != _entries.size()) {
setCheckboxStates(true);
} else {
setCheckboxStates(false);
}
}
private void setCheckboxStates(boolean checked) {
for (ImportEntry entry: _entries) {
if (entry.isChecked() != checked) {
entry.setIsChecked(checked);
}
}
}
}
@@ -0,0 +1,48 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import androidx.recyclerview.widget.RecyclerView;
public class ImportEntryHolder extends RecyclerView.ViewHolder implements ImportEntry.Listener {
private TextView _issuer;
private TextView _accountName;
private CheckBox _checkbox;
private ImportEntry _entry;
public ImportEntryHolder(final View view) {
super(view);
_issuer = view.findViewById(R.id.profile_issuer);
_accountName = view.findViewById(R.id.profile_account_name);
_checkbox = view.findViewById(R.id.checkbox_import_entry);
view.setOnClickListener(v -> _entry.setIsChecked(!_entry.isChecked()));
}
public void setData(ImportEntry entry) {
_entry = entry;
Context context = itemView.getContext();
DatabaseEntry dbEntry = entry.getDatabaseEntry();
_issuer.setText(!dbEntry.getIssuer().isEmpty() ? dbEntry.getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!dbEntry.getName().isEmpty() ? dbEntry.getName() : context.getString(R.string.unknown_account_name));
_checkbox.setChecked(entry.isChecked());
}
public ImportEntry getEntry() {
return _entry;
}
@Override
public void onCheckedChanged(boolean value) {
_checkbox.setChecked(value);
}
}
@@ -3,13 +3,14 @@ package com.beemdevelopment.aegis.ui.views;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import com.beemdevelopment.aegis.otp.TotpInfo;
import androidx.annotation.RequiresApi;
public class PeriodProgressBar extends ProgressBar {
private int _period;
@@ -1,15 +1,16 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.Slot;
import java.util.ArrayList;
import androidx.recyclerview.widget.RecyclerView;
public class SlotAdapter extends RecyclerView.Adapter<SlotHolder> {
private Listener _listener;
private ArrayList<Slot> _slots;
@@ -1,21 +1,20 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.RawSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SlotHolder extends RecyclerView.ViewHolder {
private TextView _slotUsed;
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"/>
</vector>
@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
<vector android:height="24dp" android:tint="#F7F7F7"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#F7F7F7"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
</vector>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -38,7 +38,7 @@
app:krop_aspectX="1"
app:krop_aspectY="1"
app:krop_offset="70dp"
app:krop_overlayColor="#aaffffff" >
app:krop_overlayColor="#aadddddd" >
<ImageView
android:id="@+id/iv_saveImage"
+11
View File
@@ -38,6 +38,17 @@
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_qrcode_scan"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scan_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_title="@string/scan_image"
app:fab_size="mini"
app:fab_colorNormal="@color/background"
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_add_photo_24px"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_enter"
android:layout_width="wrap_content"
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
android:id="@+id/importEntriesRootView"
tools:context="com.beemdevelopment.aegis.ui.SelectEntriesActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_entries"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:paddingBottom="60dp"
android:clipToPadding="false"
android:scrollbars="vertical"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fab_icon="@drawable/ic_check_black_24dp"
app:fab_colorNormal="@color/colorAccent"
app:fab_colorPressed="@color/colorAccent"
app:fab_labelStyle="@style/fab_label_style"
app:fab_labelsPosition="left">
</com.getbase.floatingactionbutton.FloatingActionButton>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,131 @@
<?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:layout_marginTop="20dp"
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>
</LinearLayout>
@@ -0,0 +1,76 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<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="12dp"
android:paddingBottom="16dp"
android:paddingEnd="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingStart="16dp">
<TextView
android:id="@+id/profile_issuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginTop="0dp"
android:fontFamily="sans-serif-light"
android:includeFontPadding="false"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/primaryText"
android:textSize="18sp"
android:textStyle="normal|bold"
tools:text="@string/issuer" />
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_issuer"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/extra_info_text"
android:textSize="14sp"
tools:text="AccountName" />
<CheckBox
android:id="@+id/checkbox_import_entry"
android:clickable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
@@ -7,8 +7,7 @@
android:paddingEnd="20dp"
android:paddingTop="20dp">
<EditText
android:id="@+id/text_groupname"
android:hint="@string/group_name_hint"
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" >
<item android:id="@+id/toggle_checkboxes"
android:icon="@drawable/ic_done_all_black_24dp"
android:title="@string/toggle_checkboxes"
app:showAsAction="always" />
</menu>
+157
View File
@@ -0,0 +1,157 @@
<resources>
<string name="app_name">Aegis</string>
<string name="app_name_dev">AegisDev</string>
<string name="action_settings">Paramètres</string>
<string name="action_import">Importer</string>
<string name="action_delete">Supprimer</string>
<string name="action_default_icon">Restaurer l\'icône par défaut</string>
<string name="discard">Annuler</string>
<string name="save">Enregistrer</string>
<string name="issuer">Émetteur</string>
<string name="settings">Préférences</string>
<string name="pref_appearance_group_title">Apparence</string>
<string name="pref_security_group_title">Sécurité</string>
<string name="pref_tools_group_title">Outils</string>
<string name="pref_select_theme_title">Thème</string>
<string name="pref_view_mode_title">Mode d\'affichage</string>
<string name="pref_account_name_title">Afficher le nom du compte</string>
<string name="pref_account_name_summary">Activer pour afficher le nom de compte près de l\'émetteur</string>
<string name="pref_timeout_title">Délai</string>
<string name="pref_timeout_summary">Verrouiller automatiquement la base de données après %1$s secondes d\'inactivité</string>
<string name="pref_slots_title">Emplacements de clé</string>
<string name="pref_slots_summary">Gérer la liste des clés qui peuvent déchiffrer la base de données</string>
<string name="pref_import_file_title">Importer depuis un fichier</string>
<string name="pref_import_file_summary">Importer une base de données depuis un fichier</string>
<string name="pref_import_app_title">Importer depuis une application</string>
<string name="pref_import_app_summary">Importer une base de données depuis une application (nécessite un accès root)</string>
<string name="pref_export_title">Exporter</string>
<string name="pref_export_summary">Exporter la base de données</string>
<string name="pref_secure_screen_title">Sécurité de l\'écran</string>
<string name="pref_secure_screen_summary">Bloquer les captures d\'écran et autres tentatives de capture de l\'écran dans l\'application</string>
<string name="pref_tap_to_reveal_title">Appuyer pour révéler</string>
<string name="pref_tap_to_reveal_summary">Les jetons seront masqués par défaut. Appuyer sur le jeton pour révéler le code.</string>
<string name="pref_tap_to_reveal_time_title">Délai pour appuyer pour révéler</string>
<string name="pref_auto_lock_title">Verrouillage automatique</string>
<string name="pref_auto_lock_summary">Verrouiller automatiquement lorsque vous fermez l\'application ou verrouillez votre appareil.</string>
<string name="pref_encryption_title">Chiffrement</string>
<string name="pref_encryption_summary">Chiffrer la base de données et la déverrouiller avec un mot de passe ou une empreinte digitale</string>
<string name="pref_fingerprint_title">Empreinte digitale</string>
<string name="pref_fingerprint_summary">Autoriser les empreintes digitales enregistrées sur cet appareil à déverrouiller le coffre-fort</string>
<string name="pref_set_password_title">Changer le mot de passe</string>
<string name="pref_set_password_summary">Définir un mot de passe dont vous aurez besoin pour déverrouiller votre coffre-fort</string>
<string name="fingerprint_hint">Appuyez sur le capteur</string>
<string name="fingerprint_not_recognized">Empreinte digitale non reconnue. Réessayez.</string>
<string name="fingerprint_success">Empreinte digitale reconnue</string>
<string name="choose_authentication_method">Sécurité</string>
<string name="authentication_method_none">Aucun</string>
<string name="authentication_method_none_description">Vous n\'avez pas besoin d\'un mot de passe pour déverrouiller le coffre-fort et il ne sera pas chiffré. Cette option n\'est pas recommandée.</string>
<string name="authentication_method_password">Mot de passe</string>
<string name="authentication_method_password_description">Vous avez besoin d\'un mot de passe pour déverrouiller le coffre-fort.</string>
<string name="authentication_method_fingerprint">Empreinte digitale</string>
<string name="authentication_method_fingerprint_description">En plus d\'un mot de passe, les empreintes digitales enregistrées sur cet appareil peuvent être utilisées pour déverrouiller le coffre-fort.</string>
<string name="authentication_method_set_password">Mot de passe</string>
<string name="authentication_enter_password">Saisissez votre mot de passe</string>
<string name="authentication">Déverrouiller le coffre-fort</string>
<string name="set_password">Veuillez saisir un mot de passe</string>
<string name="set_group">Veuillez saisir un nom de groupe</string>
<string name="set_number">Veuillez saisir un nombre</string>
<string name="set_password_confirm">Veuillez confirmer le mot de passe</string>
<string name="invalidated_fingerprint">Un changement dans les paramètres de sécurité de votre appareil a été détecté. Veuillez aller dans \"Paramètres -> Empreinte digitale\" et ajouter à nouveau votre empreinte digitale.</string>
<string name="unlock">Déverrouiller</string>
<string name="advanced">Avancé</string>
<string name="seconds">secondes</string>
<string name="counter">Compteur</string>
<string name="digits">Chiffres</string>
<string name="secret">Code secret</string>
<string name="scan">Scanner code QR</string>
<string name="enter_manually">Saisir manuellement</string>
<string name="add_fingerprint">Ajouter une empreinte digitale</string>
<string name="add_password">Ajouter un mot de passe</string>
<string name="slots_warning">Le coffre-fort est seulement aussi sécurisé que votre code secret le plus faible. Quand une nouvelle empreinte digitale est ajoutée à votre appareil, vous devrez réactiver l\'authentification par empreinte digitale dans Aegis.</string>
<string name="copy">Copier</string>
<string name="edit">Modifier</string>
<string name="delete">Supprimer</string>
<string name="password">Mot de passe</string>
<string name="confirm_password">Confirmer mot de passe</string>
<string name="new_profile">Nouveau profil</string>
<string name="add_new_profile">Ajouter un nouveau profil</string>
<string name="unlock_vault_error">Impossible de déverrouiller le coffre-fort</string>
<string name="unlock_vault_error_description">Mot de passe incorrect. Assurez-vous de ne pas avoir fait de faute de frappe lors de la saisie de votre mot de passe.</string>
<string name="password_equality_error">Les mots de passe doivent être identiques et non vides</string>
<string name="register_fingerprint">Enregistrer votre empreinte digitale</string>
<string name="snackbar_authentication_method">Veuillez sélectionner une méthode d\'authentification</string>
<string name="encrypting_vault">Chiffrement du coffre-fort</string>
<string name="delete_entry">Supprimer entrée</string>
<string name="delete_entry_description">Êtes-vous sûr de vouloir supprimer cette entrée ?</string>
<string name="discard_changes">Annuler les modifications ?</string>
<string name="discard_changes_description">Vos modifications n\'ont pas été enregistrées</string>
<string name="folder">Dossier</string>
<string name="tap_to_select">Appuyer pour sélectionner</string>
<string name="saving_profile_error">Erreur lors de l\'enregistrement du profil</string>
<string name="welcome">Bienvenue</string>
<string name="app_description">Aegis est une application d\'authentification à deux facteurs gratuite, sécurisée et open source</string>
<string name="setup_completed">Configuration terminée</string>
<string name="setup_completed_description">Aegis a été configuré et est prêt à être utilisé.</string>
<string name="vault_not_found">Coffre-fort introuvable, lancement de la configuration…</string>
<string name="code_copied">Code copié dans le presse-papier</string>
<string name="errors_copied">Erreurs copiées dans le presse-papier</string>
<string name="decryption_error">Une erreur est survenue en essayant de déverrouiller le coffre-fort</string>
<string name="saving_error">Une erreur est survenue en essayant d\'enregistrer le coffre-fort</string>
<string name="disable_encryption">Désactiver le chiffrement</string>
<string name="disable_encryption_description">Êtes-vous sûr de vouloir désactiver le chiffrement ? Le coffre-fort sera alors stocké en texte brut.</string>
<string name="enable_encryption_error">Une erreur est survenue lors de l\'activation du chiffrement</string>
<string name="disable_encryption_error">Une erreur est survenue lors de la désactivation du chiffrement</string>
<string name="permission_denied">Autorisation refusée</string>
<string name="choose_application">Sélectionner l\'application à partir de laquelle vous souhaitez importer une base de données</string>
<string name="choose_theme">Sélectionner le thème que vous désirez</string>
<string name="choose_view_mode">Sélectionner le mode d\'affichage que vous désirez</string>
<string name="parsing_file_error">Une erreur est survenue en essayant d\'analyser le fichier</string>
<string name="file_not_found">Erreur : Fichier introuvable</string>
<string name="reading_file_error">Une erreur est survenue en essayant de lire le fichier</string>
<string name="root_error">Erreur : impossible d\'obtenir l\'accès root</string>
<string name="imported_entries_count">%d entrées importées. %d erreurs.</string>
<string name="import_error_title">Une ou plusieurs erreurs sont survenues lors de l\'importation</string>
<string name="exporting_database_error">Une erreur est survenue en essayant d\'exporter la base de données</string>
<string name="export_database_location">La base de données a été exportée vers :</string>
<string name="export_warning">Cette action va exporter la base de données en dehors du stockage privé d\'Aegis.</string>
<string name="encryption_set_password_error">Une erreur est survenue en essayant de définir le mot de passe : </string>
<string name="encryption_enable_fingerprint_error">Une erreur est survenue en essayant d\'activer le déverrouillage par empreinte digitale : </string>
<string name="no_cameras_available">Aucun appareil photo disponible</string>
<string name="read_qr_error">Une erreur est survenue en essayant de lire le code QR</string>
<string name="authentication_method_raw">Brut</string>
<string name="unlocking_vault">Déverrouillage du coffre-fort</string>
<string name="password_slot_error">Vous devez avoir au moins un emplacement de mot de passe</string>
<string name="remove_slot">Supprimer emplacement</string>
<string name="remove_slot_description">Êtes-vous sûr de vouloir supprimer cet emplacement ?</string>
<string name="remove_group">Supprimer groupe</string>
<string name="remove_group_description">Êtes-vous sûr de vouloir supprimer ce groupe ? Les entrées dans ce groupe seront basculées automatiquement dans \"Aucun groupe\".</string>
<string name="adding_new_slot_error">Une erreur est survenue en essayant d\'ajouter un nouvel emplacement :</string>
<string name="details">Détails</string>
<string name="filter">Filtre</string>
<string name="lock">Verrouiller</string>
<string name="all">Tous</string>
<string name="name">Nom</string>
<string name="no_group">Aucun groupe</string>
<string name="sort_alphabetically">Émetteur (de A à Z)</string>
<string name="sort_alphabetically_reverse">Émetteur (de Z à A)</string>
<string name="sort_alphabetically_name">Compte (de A à Z)</string>
<string name="sort_alphabetically_name_reverse">Compte (de Z à A)</string>
<string name="sort_custom">Personnalisé</string>
<string name="new_group">Nouveau groupe…</string>
<string name="enter_group_name">Saisissez un nom de groupe</string>
<string name="group_name_hint">Nom de groupe</string>
<string name="preference_manage_groups">Modifier les groupes</string>
<string name="preference_manage_groups_summary">Gérez et supprimez vos groupes ici</string>
<string name="tap_to_reveal">Masqué</string>
<string name="selected">Sélectionné</string>
<string name="dark_theme_title">Thème sombre</string>
<string name="light_theme_title">Thème</string>
<string name="amoled_theme_title">Thème amoled </string>
<string name="normal_viewmode_title">Normal</string>
<string name="compact_mode_title">Compact</string>
<string name="small_mode_title">Petit</string>
</resources>
+153
View File
@@ -0,0 +1,153 @@
<resources>
<string name="app_name">Aegis</string>
<string name="app_name_dev">AegisDev</string>
<string name="action_settings">Instellingen</string>
<string name="action_import">Importeren</string>
<string name="action_delete">Verwijderen</string>
<string name="action_default_icon">Standaard pictogram herstellen</string>
<string name="discard">Ongedaan maken</string>
<string name="save">Opslaan</string>
<string name="issuer">Uitgever</string>
<string name="settings">Voorkeuren</string>
<string name="pref_appearance_group_title">Uiterlijk</string>
<string name="pref_security_group_title">Beveiliging</string>
<string name="pref_tools_group_title">Hulpmiddelen</string>
<string name="pref_select_theme_title">Thema</string>
<string name="pref_view_mode_title">Weergavemodus</string>
<string name="pref_account_name_title">Toon de accountnaam</string>
<string name="pref_account_name_summary">Schakel in om de accountnaam naast de uitgever te tonen</string>
<string name="pref_timeout_title">Time-out</string>
<string name="pref_timeout_summary">Automatisch database vergrendelen na %1$s seconden zonder activiteit</string>
<string name="pref_slots_title">Vergrendelingssleutels</string>
<string name="pref_slots_summary">Beheer de lijst van vergrendelingssleutels die de database kunnen decrypten</string>
<string name="pref_import_file_title">Importeren vanuit een bestand</string>
<string name="pref_import_file_summary">Database importeren vanuit een bestand</string>
<string name="pref_import_app_title">Importeren vanuit een app</string>
<string name="pref_import_app_summary">Database importeren vanuit een app (vereist root-access)</string>
<string name="pref_export_title">Exporteren</string>
<string name="pref_export_summary">Exporteer database</string>
<string name="pref_secure_screen_title">Schermbeveiliging</string>
<string name="pref_secure_screen_summary">Blokkeer screenshots en andere pogingen tot schermopnamen binnen deze app</string>
<string name="pref_tap_to_reveal_title">Drukken om te laten zien</string>
<string name="pref_tap_to_reveal_summary">Tokens zullen standaard worden verborgen. Druk op de tokens om de code te laten zien.</string>
<string name="pref_tap_to_reveal_time_title">Time-out voor drukken om te laten zien</string>
<string name="pref_encryption_title">Encryptie</string>
<string name="pref_encryption_summary">Encrypt de database en ontgrendel deze met een wachtwoord of vingerafdruk</string>
<string name="pref_fingerprint_title">Vingerafdruk</string>
<string name="pref_fingerprint_summary">Schakel in om met de op dit toestel geregistreerde vingerafdrukken de kluis te kunnen ontgrendelen</string>
<string name="pref_set_password_title">Wachtwoord wijzigen</string>
<string name="pref_set_password_summary">Stel een nieuw wachtwoord in waarmee je de kluis kunt ontgrendelen</string>
<string name="fingerprint_hint">Aanraaksensor</string>
<string name="fingerprint_not_recognized">Vingerafdruk niet herkend. Probeer opnieuw.</string>
<string name="fingerprint_success">Vingerafdruk herkend</string>
<string name="choose_authentication_method">Beveiliging</string>
<string name="authentication_method_none">Geen</string>
<string name="authentication_method_none_description">Je hebt geen wachtwoord nodig om de kluis te ontgrendelen en de kluis zal niet worden geëncrypt. Deze optie wordt niet aanbevolen.</string>
<string name="authentication_method_password">Wachtwoord</string>
<string name="authentication_method_password_description">Je hebt een wachtwoord nodig om de kluis te ontgrendelen.</string>
<string name="authentication_method_fingerprint">Vingerafdruk</string>
<string name="authentication_method_fingerprint_description">Als toevoeging op een wachtwoord kan er ook gebruik gemaakt worden van de op dit toestel geregistreerde vingerafdrukken om de kluis te ontgrendelen.</string>
<string name="authentication_method_set_password">Wachtwoord</string>
<string name="authentication_enter_password">Vul je wachtwoord in</string>
<string name="authentication">Kluis ontgrendelen</string>
<string name="set_password">Vul een wachtwoord in</string>
<string name="set_group">Vul een groepsnaam in</string>
<string name="set_number">Kies een getal</string>
<string name="set_password_confirm">Bevestig het wachtwoord</string>
<string name="invalidated_fingerprint">Er is een verandering in de beveiligingsinstellingen van je toestel gedetecteerd. Ga naar \"Instellingen -> Vingerafdruk\" om je vingerafdruk opnieuw toe te voegen.</string>
<string name="unlock">Ontgrendel</string>
<string name="advanced">Geavanceerd</string>
<string name="seconds">seconden</string>
<string name="counter">Counter</string>
<string name="digits">Cijfers</string>
<string name="secret">Sleutel</string>
<string name="scan">Scan QR-code</string>
<string name="enter_manually">Handmatig invoeren</string>
<string name="add_fingerprint">Vingerafdruk toevoegen</string>
<string name="add_password">Wachtwoord toevoegen</string>
<string name="slots_warning">De kluis is slechts even veilig als je zwakste wachtwoord. Wanneer een nieuw vingerafdruk is toegevoegd aan je toestel, moet authenticatie met vingerafdruk opnieuw worden geactiveerd binnen Aegis.</string>
<string name="copy">Kopiëren</string>
<string name="edit">Bewerken</string>
<string name="delete">Verwijderen</string>
<string name="password">Wachtwoord</string>
<string name="confirm_password">Bevestig wachtwoord</string>
<string name="new_profile">Nieuw profiel</string>
<string name="add_new_profile">Profiel toevoegen</string>
<string name="unlock_vault_error">Kluis kon niet worden ontgrendeld</string>
<string name="unlock_vault_error_description">Wachtwoord onjuist. Controleer of je het wachtwoord correct hebt ingetypt.</string>
<string name="password_equality_error">Wachtwoorden moeten overeen komen en mogen niet leeg zijn</string>
<string name="register_fingerprint">Registreer je vingerafdruk</string>
<string name="snackbar_authentication_method">Kies een inlogmethode</string>
<string name="encrypting_vault">De kluis wordt geëncrypt</string>
<string name="delete_entry">Item verwijderen</string>
<string name="delete_entry_description">Weet je zeker dat je dit item wilt verwijderen?</string>
<string name="discard_changes">Wijzigingen ongedaan maken?</string>
<string name="discard_changes_description">Je wijzigingen zijn niet opgeslagen</string>
<string name="folder">Map</string>
<string name="tap_to_select">Druk om te selecteren</string>
<string name="saving_profile_error">Fout bij opslaan profiel</string>
<string name="welcome">Welkom</string>
<string name="app_description">Aegis is een gratis en veilige open-source 2FA-app.</string>
<string name="setup_completed">Configuratie voltooid</string>
<string name="setup_completed_description">Aegis is geconfigureerd en is klaar voor gebruik.</string>
<string name="vault_not_found">Kluis niet gevonden, configuratie wordt gestart…</string>
<string name="code_copied">Code gekopieerd naar klembord</string>
<string name="errors_copied">Fouten zijn gekopieerd naar klembord</string>
<string name="decryption_error">Er is een fout opgetreden tijdens het ontgrendelen van de kluis</string>
<string name="saving_error">Er is een fout opgetreden tijdens het opslaan van de kluis</string>
<string name="disable_encryption">Encryptie uitschakelen</string>
<string name="disable_encryption_description">Weet je zeker dat je encryptie wilt uitschakelen? Dit zorgt ervoor dat de kluis in leesbare tekst wordt opgeslagen.</string>
<string name="permission_denied">Toestemming geweigerd</string>
<string name="choose_application">Selecteer de applicatie waarvan je een database wilt importeren</string>
<string name="choose_theme">Selecteer je gewenste thema</string>
<string name="choose_view_mode">Selecteer je gewenste weergavemodus</string>
<string name="parsing_file_error">Er is een fout opgetreden tijdens het vertalen van het bestand</string>
<string name="file_not_found">Fout: Bestand niet gevonden</string>
<string name="reading_file_error">Er is een fout opgetreden tijdens het lezen van het bestand</string>
<string name="root_error">Fout: Er kon geen root-access verkregen worden</string>
<string name="imported_entries_count">%d items geïmporteerd</string>
<string name="import_error_title">Er zijn fouten opgetreden tijdens het importeren</string>
<string name="exporting_database_error">Er is een fout opgetreden tijdens het exporteren van de database</string>
<string name="export_database_location">De database is geëxporteerd naar:</string>
<string name="export_warning">Deze actie zal de database uit de privé-opslag van Aegis exporteren.</string>
<string name="encryption_set_password_error">Er is een fout opgetreden tijdens het instellen van het wachtwoord: </string>
<string name="encryption_enable_fingerprint_error">Er is een fout opgetreden tijdens het inschakelen van ontgrendeling met vingerafdruk: </string>
<string name="no_cameras_available">Geen camera\'s beschikbaar</string>
<string name="read_qr_error">Er is een fout opgetreden tijdens het lezen van de QR-code</string>
<string name="authentication_method_raw">Onbewerkt</string>
<string name="unlocking_vault">Kluis wordt ontgrendeld</string>
<string name="password_slot_error">Je moet minimaal één wachtwoordvergrendeling hebben</string>
<string name="remove_slot">Vergrendelingssleutel verwijderen</string>
<string name="remove_slot_description">Weet je zeker dat je deze vergrendelingssleutel wilt verwijderen?</string>
<string name="remove_group">Groep verwijderen</string>
<string name="remove_group_description">Weet je zeker dat je deze groep wilt verwijderen? Alle items binnen deze groep worden automatisch verplaatst naar \'Geen groep\'.</string>
<string name="adding_new_slot_error">Er is een fout opgetreden tijdens het toevoegen van een nieuwe vergrendelingssleutel:</string>
<string name="details">Details</string>
<string name="filter">Filter</string>
<string name="lock">Vergrendel</string>
<string name="all">Alles</string>
<string name="name">Naam</string>
<string name="no_group">Geen groep</string>
<string name="sort_alphabetically">Uitgever (A t/m Z)</string>
<string name="sort_alphabetically_reverse">Uitgever (Z t/m A)</string>
<string name="sort_alphabetically_name">Account (A t/m Z)</string>
<string name="sort_alphabetically_name_reverse">Account (Z t/m A)</string>
<string name="sort_custom">Aangepast</string>
<string name="new_group">Nieuwe groep…</string>
<string name="enter_group_name">Vul een groepsnaam in</string>
<string name="group_name_hint">Groepsnaam</string>
<string name="preference_manage_groups">Groepen bewerken</string>
<string name="preference_manage_groups_summary">Beheer en verwijder hier je groepen</string>
<string name="tap_to_reveal">Verborgen</string>
<string name="dark_theme_title">Donker thema</string>
<string name="light_theme_title">Licht thema</string>
<string name="amoled_theme_title">Amoled thema</string>
<string name="selected">Geselecteerd</string>
<string name="normal_viewmode_title">Normaal</string>
<string name="compact_mode_title">Compact</string>
<string name="small_mode_title">Klein</string>
</resources>
+5 -11
View File
@@ -8,9 +8,11 @@
<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_appearance_group_title">Appearance</string>
<string name="pref_security_group_title">Security</string>
<string name="pref_tools_group_title">Tools</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>
@@ -57,14 +59,6 @@
<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>
@@ -106,7 +100,7 @@
<string name="saving_error">Произошла ошибка при попытке сохранить хранилище</string>
<string name="disable_encryption">Отключить шифрование</string>
<string name="disable_encryption_description">Вы уверены что хотите отключить шифрование? Это приведет к хранению хранилища в виде простого текста.</string>
<string name="encrypting_error">Произошла ошибка при включении шифрования</string>
<string name="enable_encryption_error">Произошла ошибка при включении шифрования</string>
<string name="permission_denied">Доступ запрещен</string>
<string name="choose_application">Выберите приложение из которого вы хотите импортировать базу данных.</string>
<string name="choose_theme">Выберите тему</string>
@@ -114,7 +108,7 @@
<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="imported_entries_count">Импортироаано %d записей</string>
<string name="exporting_database_error">Произошла ошибка при попытке экспорта базы данных</string>
<string name="export_database_location">База данных была экспортирована в:</string>
<string name="export_warning">Это действие экспортирует базу данных из личного хранилища Aegis.</string>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="authentication_methods">
<item>@string/authentication_method_none</item>
<item>@string/authentication_method_password</item>
<item>@string/authentication_method_fingerprint</item>
</string-array>
<string-array name="otp_types_array">
<item>TOTP</item>
<item>HOTP</item>
<item>Steam</item>
</string-array>
<string-array name="otp_algo_array">
<item>SHA1</item>
<item>SHA256</item>
<item>SHA512</item>
</string-array>
<string-array name="theme_titles">
<item>@string/light_theme_title</item>
<item>@string/dark_theme_title</item>
<item>@string/amoled_theme_title</item>
</string-array>
<string-array name="view_mode_titles">
<item>@string/normal_viewmode_title</item>
<item>@string/compact_mode_title</item>
<item>@string/small_mode_title</item>
</string-array>
</resources>
+28 -26
View File
@@ -8,9 +8,11 @@
<string name="discard">Discard</string>
<string name="save">Save</string>
<string name="issuer">Issuer</string>
<string name="title_activity_intro">IntroActivity</string>
<string name="settings">Preferences</string>
<string name="pref_appearance_group_title">Appearance</string>
<string name="pref_security_group_title">Security</string>
<string name="pref_tools_group_title">Tools</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>
@@ -30,6 +32,8 @@
<string name="pref_tap_to_reveal_title">Tap to reveal</string>
<string name="pref_tap_to_reveal_summary">Tokens will be hidden by default. Tap on the tokens to reveal code.</string>
<string name="pref_tap_to_reveal_time_title">Timeout for tap to reveal</string>
<string name="pref_auto_lock_title">Auto lock</string>
<string name="pref_auto_lock_summary">Automatically lock when you close the app or lock your device.</string>
<string name="pref_encryption_title">Encryption</string>
<string name="pref_encryption_summary">Encrypt the database and unlock it with a password or fingerprint</string>
<string name="pref_fingerprint_title">Fingerprint</string>
@@ -57,26 +61,6 @@
<string name="set_password_confirm">Please confirm the password</string>
<string name="invalidated_fingerprint">A change in your device\'s security settings has been detected. Please go to \"Settings -> Fingerprint\" and re-add your fingerprint.</string>
<string-array name="authentication_methods">
<item>None</item>
<item>Password</item>
<item>Fingerprint</item>
</string-array>
<string-array name="otp_types_array">
<item>TOTP</item>
<item>HOTP</item>
<item>Steam</item>
</string-array>
<string-array name="otp_algo_array">
<item>SHA1</item>
<item>SHA256</item>
<item>SHA512</item>
</string-array>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="unlock">Unlock</string>
<string name="advanced">Advanced</string>
<string name="seconds">seconds</string>
@@ -84,10 +68,11 @@
<string name="digits">Digits</string>
<string name="secret">Secret</string>
<string name="scan">Scan QR code</string>
<string name="scan_image">Scan image</string>
<string name="enter_manually">Enter manually</string>
<string name="add_fingerprint">Add fingerprint</string>
<string name="add_password">Add password</string>
<string name="slots_warning">The vault is only as secure as your weakest secret. When a new fingerprint is added to your device, you will to reactivate fingerprint authentication within Aegis.</string>
<string name="slots_warning">The vault is only as secure as your weakest secret. When a new fingerprint is added to your device, you will need to reactivate fingerprint authentication within Aegis.</string>
<string name="copy">Copy</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
@@ -97,7 +82,7 @@
<string name="add_new_profile">Add new profile</string>
<string name="unlock_vault_error">Couldn\'t unlock vault</string>
<string name="unlock_vault_error_description">Incorrect password. Make sure you didn\'t mistype your password.</string>
<string name="password_equality_error">Passwords should be equal and non-empty</string>
<string name="password_equality_error">Passwords should be identical and non-empty</string>
<string name="register_fingerprint">Register your fingerprint</string>
<string name="snackbar_authentication_method">Please select an authentication method</string>
<string name="encrypting_vault">Encrypting the vault</string>
@@ -119,7 +104,8 @@
<string name="saving_error">An error occurred while trying to save the vault</string>
<string name="disable_encryption">Disable encryption</string>
<string name="disable_encryption_description">Are you sure you want to disable encryption? This will cause the vault to be stored in plain text.</string>
<string name="encrypting_error">An error occurred while enabling encryption</string>
<string name="enable_encryption_error">An error occurred while enabling encryption</string>
<string name="disable_encryption_error">An error occurred while disabling 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>
@@ -127,8 +113,10 @@
<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="app_lookup_error">Error: App is not installed</string>
<string name="root_error">Error: unable to obtain root access</string>
<string name="imported_entries_count">Imported %d entries. %d errors.</string>
<string name="imported_entries_count">Imported %d entries</string>
<string name="read_entries_count">Read %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>
@@ -145,6 +133,7 @@
<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="progressbar_error">Unable to reset animator duration scale. Progress bars will be invisible</string>
<string name="details">Details</string>
<string name="filter">Filter</string>
<string name="lock">Lock</string>
@@ -156,10 +145,23 @@
<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="new_group">New group</string>
<string name="enter_group_name">Enter a group name</string>
<string name="group_name_hint">Group name</string>
<string name="preference_manage_groups">Edit groups</string>
<string name="preference_manage_groups_summary">Manage and delete your groups here</string>
<string name="tap_to_reveal">Hidden</string>
<string name="selected">Selected</string>
<string name="dark_theme_title">Dark theme</string>
<string name="light_theme_title">Light theme</string>
<string name="amoled_theme_title">Amoled theme</string>
<string name="normal_viewmode_title">Normal</string>
<string name="compact_mode_title">Compact</string>
<string name="small_mode_title">Small</string>
<string name="unknown_issuer">Unknown issuer</string>
<string name="unknown_account_name">Unknown account name</string>
<string name="import_error_dialog">Aegis could not import %d tokens. These tokens will be skipped. Press \'details\' to see more information about the errors.</string>
<string name="unable_to_read_qrcode">Unable to read and process QR code</string>
<string name="select_picture">Select picture</string>
<string name="toggle_checkboxes">Toggle checkboxes</string>
</resources>
+9 -4
View File
@@ -5,7 +5,7 @@
android:title="@string/settings">
<PreferenceCategory
android:title="Appearance"
android:title="@string/pref_appearance_group_title"
app:iconSpaceReserved="false">
<Preference
android:defaultValue="false"
@@ -34,7 +34,7 @@
</PreferenceCategory>
<PreferenceCategory
android:title="Security"
android:title="@string/pref_security_group_title"
app:iconSpaceReserved="false">
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="true"
@@ -42,13 +42,18 @@
android:title="@string/pref_secure_screen_title"
android:summary="@string/pref_secure_screen_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="true"
android:key="pref_auto_lock"
android:title="@string/pref_auto_lock_title"
android:summary="@string/pref_auto_lock_summary"
app:iconSpaceReserved="false"/>
<androidx.preference.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_tap_to_reveal"
android:title="@string/pref_tap_to_reveal_title"
android:summary="@string/pref_tap_to_reveal_summary"
app:iconSpaceReserved="false"/>
<Preference
android:key="pref_tap_to_reveal_time"
android:title="@string/pref_tap_to_reveal_time_title"
@@ -92,7 +97,7 @@
</PreferenceCategory>
<PreferenceCategory
android:title="Tools"
android:title="@string/pref_tools_group_title"
app:iconSpaceReserved="false">
<Preference
android:key="pref_import"
@@ -1,13 +1,13 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import org.junit.Test;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import static org.junit.Assert.*;
public class HOTPTest {
@@ -0,0 +1,40 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.junit.Test;
import javax.crypto.SecretKey;
import static org.junit.Assert.*;
public class SCryptTest {
@Test
public void testTrailingNullCollision() throws HexException {
byte[] salt = new byte[0];
SCryptParameters params = new SCryptParameters(
CryptoUtils.CRYPTO_SCRYPT_N,
CryptoUtils.CRYPTO_SCRYPT_p,
CryptoUtils.CRYPTO_SCRYPT_r,
salt
);
byte[] expectedKey = Hex.decode("41cd8110d0c66ede16f97ce84fd8e2bd2269c9318532a01437789dfbadd1392e");
byte[][] inputs = new byte[][]{
new byte[]{'t', 'e', 's', 't'},
new byte[]{'t', 'e', 's', 't', '\0'},
new byte[]{'t', 'e', 's', 't', '\0', '\0'},
new byte[]{'t', 'e', 's', 't', '\0', '\0', '\0'},
new byte[]{'t', 'e', 's', 't', '\0', '\0', '\0', '\0'},
new byte[]{'t', 'e', 's', 't', '\0', '\0', '\0', '\0', '\0'},
};
for (byte[] input : inputs) {
SecretKey key = CryptoUtils.deriveKey(input, params);
assertArrayEquals(expectedKey, key.getEncoded());
}
}
}
@@ -1,14 +1,11 @@
package com.beemdevelopment.aegis;
import org.junit.Test;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.junit.Test;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

+264
View File
@@ -0,0 +1,264 @@
# Aegis Vault
Aegis persists the user's tokens to a file. This file is referred to as the
__vault__. Users can configure the app to store the vault in plain text or to
encrypt it with a password.
This document describes Aegis' security design and file format. It's split up
into two parts. First, the cryptographic primitives and use of them for
encryption are discussed. The second section documents the details of the file
format of the vault.
## Security
### Primitives
Two cryptographic primitives were selected for use in Aegis. An Authenticated
Encryption with Associated Data (AEAD) cipher and a Key Derivation Function
(KDF).
#### AEAD
__AES-256__ in __GCM__ mode is used as the AEAD cipher to ensure the
confidentility, integrity and authenticity of the vault contents.
It requires a unique 96-bit nonce for each invocation with the same key.
However, it is not possible to use a monotically increasing counter for this in
this case, because a future use case could involve using the vault on multiple
devices simultaneously, which would almost certainly result in nonce reuse. This
is suboptimal, because 96 bits is not large enough to comfortably generate an
unlimited amount of random numbers without getting collisions at some point
either. As a repeat of the nonce would have catastrophic consequences for the
confidentiality and integrity of the ciphertext, NIST strongly recommends not
exceeding 2<sup>32</sup> invocations when using random nonces with GCM. As such,
the security of the Aegis vault also relies on the assumption that this limit is
never exceeded. In the case of Aegis, this is a reasonable assumption to make,
as it's highly unlikely that a user will ever come close to saving the vault
2<sup>32</sup> times.
_Switching to a nonce misuse-resistant cipher like AES-GCM-SIV or a cipher with
a larger (192 bits) nonce like XChaCha-Poly1305 will be explored in the future._
#### KDF
__scrypt__ is used as the KDF to derive a key from a user-provided password,
with the following parameters:
| Parameter | Value |
| :-------- | :------------- |
| N | 2<sup>15</sup> |
| r | 8 |
| p | 1 |
These are the same parameters as Android itself uses to derive a key for
full-disk encryption. Because of the memory limitations Android apps have, it's
not possible to increase them without running into OOM conditions on most
devices.
_Argon2 is a more modern KDF that provides an advantage over scrypt because it
allows tweaking the memory-hardness parameter and CPU-hardness parameter
separately, whereas scrypt ties those together into one cost parameter (N). As
many applications have started using Argon2 in production, it seems that it has
withstood the test of time. It will be considered as an alternative option to
switch to in the future._
### Encryption
When a vault is first created, a random 256-bit key is generated that is used to
encrypt the contents with AES in GCM mode. This key is referred to as the
__master key__.
Aegis supports unlocking a vault with multiple different credentials. The main
credential is a key derived from a user-provided password. In addition to that,
users can also add a key backed by the Android KeyStore (authorized by the scan
of a fingerprint) as a credential.
#### Slots
Each credential that should be able to encrypt/decrypt the contents of a vault
has its own __slot__. Every slot contains a copy of the master key that is
encrypted with its credential. The process of encrypting a key with another key
is known as __key wrapping__. This allows obtaining the master key by providing
any of the credentials. An important consequence is that the master key is only
as secure as the weakest credential.
This design is similar to and largely inspired by LUKS' key slot system.
#### Integrity
Because of the use of an AEAD for encryption, the vault contents and encrypted
master keys in the slots are checked for integrity and authenticity. The rest of
the file is not.
### Overview
An attempt was made to create a clear overview of the encryption system.
![](diagram.svg)
## Format
The vault is stored in JSON and encoded in UTF-8. The upper-level structure is
shown below:
```json
{
"version": 1,
"header": {},
"db": {}
}
```
It starts with a ``version`` number and a ``header``. If a backwards
incompatible change is introduced to the content format, the version number will
be incremented. The vault contents are stored under ``db``. Its value depends on
wheter the vault is encrypted or not. If it is, the value is a string containing
the Base64 encoded (with padding) ciphertext of the vault contents. Otherwise,
the value is a JSON object.
Full examples of a plain text vault and an encrypted vault are available in the
[testdata](/testdata) folder. There's also a Python script that can decrypt an
Aegis vault given the password: [scripts/decrypt.py](/scripts/decrypt.py).
### Header
The header starts with the list of ``slots``. It also has a ``params`` object
that holds the ``nonce`` and ``tag`` that were produced during encryption,
encoded as a hexadecimal string.
Setting ``slots`` and ``params`` to null indicates that the vault is not
encrypted and Aegis will try to parse it as such.
```json
{
"slots": [],
"params": {
"nonce": "0123456789abcdef01234567",
"tag": "0123456789abcdef0123456789abcdef"
}
}
```
#### Slots
The different slot types are identified with a numerical ID.
| Type | ID |
| :---------- | :--- |
| Raw | 0x00 |
| Fingerprint | 0x01 |
| Password | 0x02 |
##### Raw
This slot type is used for raw AES key credentials. It is not used directly in
the app, but all other slots are based on this slot type, so this section
applies to all of them.
Each slot transforms its credential in a way that it can be used to encrypt the
master key with AES-256 in GCM mode. The ``nonce``, ``tag`` and encrypted
``key`` are encoded as a hexadecimal string and stored together. Slots also have
a unique randomly generated ``UUID`` (version 4).
```json
{
"type": 0,
"uuid": "01234567-89ab-cdef-0123-456789abcdef",
"key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"key_params": {
"nonce": "0123456789abcdef01234567",
"tag": "0123456789abcdef0123456789abcdef"
}
}
```
##### Fingerprint
The structure of the Fingerprint slot is exactly the same as the Raw slot. The
difference is that the wrapper key is backed by the Android KeyStore, whereas
Raw slots don't imply use of a particular storage type.
##### Password
As noted earlier, scrypt is used to derive a 256-bit key from a user-provided
password. A random 256-bit ``salt`` is generated and passed to scrypt to protect
against rainbow table attacks. Its stored along with the ``N``, ``r`` and ``p``
parameters.
```json
{
"type": 1,
"uuid": "01234567-89ab-cdef-0123-456789abcdef",
"key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"key_params": {
"nonce": "0123456789abcdef01234567",
"tag": "0123456789abcdef0123456789abcdef"
},
"n": 32768,
"r": 8,
"p": 1,
"salt": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
```
### Content
The content is a JSON object encoded in UTF-8.
```json
{
"version": 1,
"entries": []
}
```
It has a ``version`` number and a list of ``entries``. If a backwards
incompatible change is introduced to the content format, the version number will
be incremented.
#### Entries
Each entry has a unique randomly generated ``UUID`` (version 4), as well as a
``name`` and ``issuer`` to idenfity the account name and service that the token
is for. Entries can also have an icon. These are JPEG's encoded in Base64 with
padding. The ``info`` object holds information specific to the OTP type. The
``secret`` is encoded in Base32 without padding.
There are a number of supported types:
| Type | ID |
| :------------------ | :------ |
| TOTP | "totp" |
| HOTP | "hotp" |
| Steam | "steam" |
There is no specification available for Steam's OTP algorithm. It's essentially
the same as TOTP, but it uses a different final encoding step. Aegis's
implementation of it can be found in
[crypto/otp/OTP.java](https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java).
The following algorithms are supported for all OTP types:
| Algorithm | ID |
| :-------- | :------- |
| SHA-1 | "SHA1" |
| SHA-256 | "SHA256" |
| SHA-512 | "SHA512" |
Example of a TOTP entry:
```json
{
"type": "totp",
"uuid": "01234567-89ab-cdef-0123-456789abcdef",
"name": "Bob",
"issuer": "Google",
"icon": null,
"info": {
"secret": "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
"algo": "SHA1",
"digits": 6,
"period": 30
}
}
```
+7 -7
View File
@@ -1,18 +1,18 @@
Aegis Authenticator is a free, secure and open source app to manage your
2-step verification tokens for your online services.
'''Encryption'''
<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.
'''Fingerprint unlock'''
<b>Fingerprint unlock</b>
Entering your password each time you need access to a
one-time password can be cumbersome. Fortunately, you can also enable
fingerprint unlock if your device has a fingerprint scanner.
'''Compatibility'''
<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
@@ -21,19 +21,19 @@ and Instagram.
It is also compatible with Google Authenticator. Any website that shows a QR
code for Google Authenticator also works with Aegis.
'''Groups'''
<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.
'''Backups'''
<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.
'''Open source and license'''
<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
'''Attribution'''
<b>Attribution</b>
The icon is largely based on a design by Freepik from Flaticon.
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# this depends on the 'cryptography' package
# pip install cryptography
# example usage: ./scripts/decrypt.py --input ./testdata/aegis_export.json
# password: test
import argparse
import base64
import getpass
import io
import json
import sys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backend
import cryptography
backend = default_backend()
def die(msg, code=1):
print(msg, file=sys.stderr)
exit(code)
def main():
parser = argparse.ArgumentParser(description="Decrypt an Aegis vault")
parser.add_argument("--input", dest="input", required=True, help="encrypted Aegis vault file")
parser.add_argument("--output", dest="output", default="-", help="output file ('-' for stdout)")
args = parser.parse_args()
# parse the Aegis vault file
with io.open(args.input, "r") as f:
data = json.load(f)
# ask the user for a password
password = getpass.getpass().encode("utf-8")
# extract all password slots from the header
header = data["header"]
slots = [slot for slot in header["slots"] if slot["type"] == 1]
# try the given password on every slot until one succeeds
master_key = None
for slot in slots:
# derive a key from the given password
kdf = Scrypt(
salt=bytes.fromhex(slot["salt"]),
length=32,
n=slot["n"],
r=slot["r"],
p=slot["p"],
backend=backend
)
key = kdf.derive(password)
# try to use the derived key to decrypt the master key
cipher = AESGCM(key)
params = slot["key_params"]
try:
master_key = cipher.decrypt(
nonce=bytes.fromhex(params["nonce"]),
data=bytes.fromhex(slot["key"]) + bytes.fromhex(params["tag"]),
associated_data=None
)
break
except cryptography.exceptions.InvalidTag:
pass
if master_key is None:
die("error: unable to decrypt the master key with the given password")
# decode the base64 vault contents
content = base64.b64decode(data["db"])
# decrypt the vault contents using the master key
params = header["params"]
cipher = AESGCM(master_key)
db = cipher.decrypt(
nonce=bytes.fromhex(params["nonce"]),
data=content + bytes.fromhex(params["tag"]),
associated_data=None
)
db = db.decode("utf-8")
if args.output != "-":
with io.open(args.output, "w") as f:
f.write(db)
else:
print(db)
if __name__ == "__main__":
main()
+8 -8
View File
File diff suppressed because one or more lines are too long