Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c222cffa | ||
|
|
57e3574693 | ||
|
|
98c166a6b2 | ||
|
|
b6a31e798f | ||
|
|
5ebb714d05 | ||
|
|
9958f73926 | ||
|
|
0fb33744ae | ||
|
|
6d26d1beb0 | ||
|
|
0a8dd56306 | ||
|
|
8a8d714901 | ||
|
|
c2cb5a3044 | ||
|
|
9893b2f057 | ||
|
|
83b2f85ff4 | ||
|
|
47df8968c7 | ||
|
|
33b4b4b7ab | ||
|
|
21fd8fdd8d | ||
|
|
7ec8391872 | ||
|
|
afb9e59711 | ||
|
|
24a93ecc9f | ||
|
|
2de9b96138 | ||
|
|
e0d5aba94a | ||
|
|
f3b4433d5b | ||
|
|
f4678e2813 | ||
|
|
b958145761 | ||
|
|
feea9a2e67 | ||
|
|
ebb9d0be3f | ||
|
|
f5cbec21f4 | ||
|
|
e55d6111cf | ||
|
|
cb01fb21d3 | ||
|
|
c5be99e58d | ||
|
|
92458d0d3b | ||
|
|
5d835b05ee | ||
|
|
2a47fa4de5 | ||
|
|
ead17949f9 | ||
|
|
d5a848e79e | ||
|
|
553cb16bc4 | ||
|
|
47f8c9e648 | ||
|
|
791138ccab | ||
|
|
6cbef27cfb | ||
|
|
e5f7dce9b8 | ||
|
|
a415ef573e | ||
|
|
ffa2577cc2 | ||
|
|
2a0daae155 | ||
|
|
b41e328d22 | ||
|
|
1ac42d85d6 | ||
|
|
685a791aba | ||
|
|
89a1663e28 | ||
|
|
0563ac917a | ||
|
|
719ef3bc71 | ||
|
|
18fd88a441 | ||
|
|
6d93b78f9a | ||
|
|
4365a693f2 | ||
|
|
14fa493ac9 | ||
|
|
36e3dd559c | ||
|
|
b681d57b4e | ||
|
|
c5fa40166f | ||
|
|
b7cf234db6 |
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
-1
@@ -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
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+75
-57
@@ -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;
|
||||
}
|
||||
}
|
||||
+71
-63
@@ -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;
|
||||
|
||||
+14
-16
@@ -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();
|
||||
|
||||
+5
-5
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
+1
-2
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
+264
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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,
|
||||
it’s 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.
|
||||
|
||||
Executable
+93
@@ -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()
|
||||
Vendored
+8
-8
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user