Compare commits

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

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

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

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

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

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

Move importing logic back to the PreferencesFragment

Add minor changes

Add ImportEntry to properly track checked states

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

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

It's the first step towards implementing #7
2019-04-07 18:36:13 +02:00
Alexander Bakker 6d93b78f9a Fix a crash on KitKat when clearing the KeyStore
Apparently KitKat doesn't like KeyPermanentlyInvalidatedException
2019-04-04 18:49:57 +02:00
Alexander Bakker 4365a693f2 Get Cipher back from FingerprintManager,CryptoObject after auth 2019-04-04 18:18:34 +02:00
Alexander Bakker 14fa493ac9 Remove keys from the KeyStore when disabling encryption or fingerprint 2019-04-04 15:27:24 +02:00
Alexander Bakker 36e3dd559c Normalize formatting and 'optimize' imports 2019-04-04 14:07:36 +02:00
Alexander Bakker b681d57b4e Update testdata file to use 'test' as the password 2019-04-03 15:03:56 +02:00
Alexander Bakker c5fa40166f Add some info about verifying APK's obtained from GitHub to the README 2019-04-02 21:37:49 +02:00
Michael Schättgen b7cf234db6 Add smaller view mode
Closes #39
2019-04-02 15:47:07 +02:00
Alexander Bakker f27dc54ec4 Release v0.3 2019-04-01 23:50:07 +02:00
Alexander Bakker 4c1dec613c Fallback to a 30 second period if an andOTP doesn't provide one 2019-04-01 23:48:44 +02:00
Alexander Bakker a251685002 Add a dialog that displays a list of errors that occurred during import 2019-04-01 23:36:31 +02:00
Alexander Bakker 118221e3df Add support for Steam tokens to the andOTP importer
Close #44
2019-04-01 22:37:56 +02:00
Alexander Bakker 1dd5f893da Add support for Steam OTP 2019-04-01 22:37:52 +02:00
Michael Schättgen a46640f43d Fix recreate when setting view mode 2019-04-01 21:47:41 +02:00
Michael Schättgen a293078603 Add way to set view mode 2019-04-01 01:44:38 +02:00
Michael Schättgen f3ed79dc71 Add ability to change view mode 2019-04-01 01:21:12 +02:00
Michael Schättgen ee8fd2e9f2 Hide divider in compact mode when progressbar is visible 2019-04-01 01:04:26 +02:00
Michael Schättgen 8b9ec5f976 Start working on compact mode 2019-04-01 00:58:45 +02:00
Michael Schättgen d283eae7a0 Add russian strings for authentication methods
#43
2019-04-01 00:18:44 +02:00
Michael SchättgenandGitHub a854db760c Merge pull request #41 from ferhadnecef/stringlate-ru-1048
Updated ru (Russian) translation
2019-03-31 23:57:36 +02:00
Michael Schättgen caf2ec882a Merge branch 'feature-sorting' 2019-03-31 23:30:52 +02:00
Michael Schättgen bbf967e2b7 Add a way to save current sorting method 2019-03-31 22:34:25 +02:00
Michael Schättgen b9d011b48e Disable drag and drop when list is sorted 2019-03-31 21:28:37 +02:00
Michael Schättgen ecdadf5335 Add SortCategory fixes and support for custom sorting 2019-03-31 21:23:14 +02:00
Michael Schättgen 10b80442d0 Comparators will now ignore casing 2019-03-31 21:22:20 +02:00
Alexander Bakker a2879a4558 Fix the screenshot links in the README 2019-03-31 20:39:41 +02:00
Michael Schättgen 62025d1053 Add way to check for reversed sort categories 2019-03-31 20:14:11 +02:00
Michael Schättgen 09fe550a28 Add AccountNameComparator 2019-03-31 20:12:34 +02:00
Michael Schättgen 07aac1e6a3 Add support for reversed sorting methods 2019-03-31 20:12:25 +02:00
Michael Schättgen b8939b504e Start working on entry sorting 2019-03-31 19:57:45 +02:00
Alexander Bakker 4f29d0c2e4 Fix metadata files for F-Droid (for real this time) 2019-03-31 18:21:10 +02:00
Alexander Bakker 592c6683c3 Continue importing entries even if one can't be parsed 2019-03-30 18:26:16 +01:00
ferhad.necef f8edac72ea Updated ru (Russian) translation 2019-03-29 16:00:49 +03:00
Alexander Bakker fc0e1150f6 Replace hardcoded encoding names with constants 2019-03-28 20:01:00 +01:00
Michael SchättgenandGitHub e1fd913817 Update README.md 2019-03-28 13:15:18 +00:00
Michael Schättgen ede9f93a59 Release v0.2.2 2019-03-28 13:46:39 +01:00
Michael Schättgen c6c0eb88e9 Fix true black theme in all activities
Closes #32. Also tested on API 19.
2019-03-28 13:46:33 +01:00
Michael Schättgen f929790be6 Fix metadata path by adding android folder
Closes #36
2019-03-28 13:46:07 +01:00
Michael Schättgen a4ff9d6d31 Fix crash in API 19 2019-03-28 13:45:43 +01:00
Michael SchättgenandGitHub a21d28a91f Update README.md 2019-03-28 10:47:24 +00:00
Michael Schättgen f7c32bc28b Improve theme based texts 2019-03-28 01:27:07 +01:00
Michael Schättgen 59c0ca947d Add workaround for amoled themed preferences 2019-03-28 01:08:44 +01:00
Michael Schättgen a7ac1adb80 Convert dark mode preference to select theme preference 2019-03-28 00:56:29 +01:00
Michael Schättgen 17204cc026 Add status bar color for true dark theme 2019-03-28 00:54:50 +01:00
Michael Schättgen cf4aecbd3e Refactor dark mode to allow multiple themes 2019-03-28 00:54:30 +01:00
Michael Schättgen 9baaf824c6 Start working on amoled / true black theme
#32
2019-03-28 00:15:02 +01:00
Michael Schättgen eb172fc54e Merge branch 'master' of https://github.com/alexbakker/Aegis 2019-03-27 21:18:26 +01:00
Michael Schättgen e99762b814 Change global progressbar color to improve visibility
As part of #34
2019-03-27 21:18:16 +01:00
Alexander Bakker 6b1d4a60e3 Credit Freepik from Flaticon for the icon 2019-03-27 12:32:40 +01:00
Alexander Bakker a540b3cfee Add icon to fastlane metadata 2019-03-27 12:14:08 +01:00
Michael SchättgenandGitHub b0481d6b30 Update README.md 2019-03-27 11:07:03 +00:00
Alexander Bakker 0c315c53e5 Add .idea/ and release/ to .gitignore 2019-03-27 01:03:55 +01:00
123 changed files with 4023 additions and 1256 deletions
+2 -2
View File
@@ -16,6 +16,7 @@ out/
# Gradle files
.gradle/
build/
release/
# Local configuration file (sdk path, etc)
local.properties
@@ -34,8 +35,7 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
.idea/libraries
.idea/
# Keystore files
*.jks
Generated
-1
View File
@@ -1 +0,0 @@
Aegis
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>
-3
View File
@@ -1,3 +0,0 @@
<component name="CopyrightManager">
<settings default="" />
</component>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<compositeConfiguration>
<compositeBuild compositeDefinitionSource="SCRIPT" />
</compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="F:\Android\Android Studio\gradle\gradle-2.14.1" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
-44
View File
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="10">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="4" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="9">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/Aegis.iml" filepath="$PROJECT_DIR$/Aegis.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+71 -15
View File
@@ -2,63 +2,119 @@
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
- Scan QR code
- Enter details manually
- Import
- Import from files
- andOTP
- FreeOTP
- 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
- Drag and drop
- Custom groups
- Advanced entry editing
- Material design
- Light and dark themes
- Material design with multiple themes:
- Light theme
- Dark theme
- Amoled / true dark theme
- Export (plaintext or encrypted)
## Screenshots
[<img width=200 alt="Main Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot_edit.png?raw=true).png
[<img width=200 alt="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
The icon is largely based on a design by Freepik from Flaticon.
+3 -3
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 4
versionName "0.2.1"
versionCode 11
versionName "0.4.1"
}
buildTypes {
debug {
@@ -41,7 +41,7 @@ dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:v4.2.2'
implementation 'com.github.avito-tech:krop:3e65e12'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation 'com.mattprecious.swirl:swirl:1.0.0'
implementation 'de.hdodenhof:circleimageview:2.2.0'
+28 -38
View File
@@ -3,61 +3,51 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="com.beemdevelopment.aegis.AegisApplication"
android:name=".AegisApplication"
android:allowBackup="true"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
tools:replace="android:theme"
android:theme="@style/AppTheme.NoActionBar">
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:theme">
<activity
android:name="com.beemdevelopment.aegis.ui.MainActivity"
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}"
android:alwaysRetainTaskState="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.beemdevelopment.aegis.ui.ScannerActivity"
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="Scan a QR code"
android:theme="@style/AppTheme.Fullscreen"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize">
</activity>
android:screenOrientation="portrait" />
<activity
android:name="com.beemdevelopment.aegis.ui.EditEntryActivity"
android:label="Edit profile"
android:theme="@style/AppTheme.NoActionBar">
</activity>
android:name=".ui.EditEntryActivity"
android:label="Edit profile" />
<activity
android:name="com.beemdevelopment.aegis.ui.IntroActivity"
android:theme="@style/Theme.Intro"
android:launchMode="singleTop">
</activity>
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
<activity
android:name="com.beemdevelopment.aegis.ui.AuthActivity"
android:launchMode="singleTop">
</activity>
<activity android:name="com.beemdevelopment.aegis.ui.PreferencesActivity">
</activity>
android:name=".ui.SlotManagerActivity"
android:label="Manage key slots" />
<activity
android:name="com.beemdevelopment.aegis.ui.SlotManagerActivity"
android:label="Manage key slots"
android:theme="@style/AppTheme.TransparentActionBar">
</activity>
<activity
android:name="com.beemdevelopment.aegis.ui.GroupManagerActivity"
android:label="Manage groups"
android:theme="@style/AppTheme.TransparentActionBar">
</activity>
android:name=".ui.GroupManagerActivity"
android:label="Manage groups" />
</application>
</manifest>
</manifest>
@@ -1,7 +1,10 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
@@ -10,19 +13,27 @@ import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.ui.MainActivity;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.RequiresApi;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
registerReceiver(receiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
@@ -37,6 +48,25 @@ public class AegisApplication extends Application {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
}
public void registerLockListener(LockListener listener) {
_lockListeners.add(listener);
}
public void unregisterLockListener(LockListener listener) {
_lockListeners.remove(listener);
}
public void lock() {
_manager.lock();
for (LockListener listener : _lockListeners) {
listener.onLocked();
}
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
@@ -57,4 +87,17 @@ public class AegisApplication extends Application {
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
}
}
}
public interface LockListener {
void onLocked();
}
}
@@ -11,10 +11,6 @@ public class Preferences {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public boolean isDarkModeEnabled() {
return _prefs.getBoolean("pref_dark_mode", false);
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
@@ -31,6 +27,10 @@ public class Preferences {
return _prefs.getBoolean("pref_intro", false);
}
public boolean isAutoLockEnabled() {
return _prefs.getBoolean("pref_auto_lock", true);
}
public void setIntroDone(boolean done) {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
@@ -39,10 +39,33 @@ public class Preferences {
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
}
public void setCurrentSortCategory(SortCategory category) {
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
}
public 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 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 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);
@@ -0,0 +1,64 @@
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,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED;
private static SortCategory[] _values;
static {
_values = values();
}
public static SortCategory fromInteger(int x) {
return _values[x];
}
public Comparator<DatabaseEntry> getComparator() {
Comparator<DatabaseEntry> comparator = null;
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator();
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator());
break;
case ISSUER:
comparator = new IssuerNameComparator();
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator());
break;
}
return comparator;
}
public int getMenuItem() {
switch (this) {
case CUSTOM:
return R.id.menu_sort_custom;
case ACCOUNT:
return R.id.menu_sort_alphabetically_name;
case ACCOUNT_REVERSED:
return R.id.menu_sort_alphabetically_name_reverse;
case ISSUER:
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
default:
return R.id.menu_sort_custom;
}
}
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED;
private static Theme[] _values;
static {
_values = values();
}
public static Theme fromInteger(int x) {
return _values[x];
}
}
@@ -0,0 +1,33 @@
package com.beemdevelopment.aegis;
import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL;
private static ViewMode[] _values;
static {
_values = values();
}
public static ViewMode fromInteger(int x) {
return _values[x];
}
@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;
}
}
}
@@ -2,11 +2,13 @@ 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;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -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 = Charset.forName("UTF-8").encode(charBuf);
return byteBuf.array();
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
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;
@@ -12,7 +12,7 @@ public class HOTP {
private HOTP() {
}
public static String generateOTP(byte[] secret, String algo, int digits, long counter)
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
@@ -30,18 +30,11 @@ public class HOTP {
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
long bin = ((hash[offset] & 0x7f) << 24)
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
long otp = bin % (long) Math.pow(10, digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(otp));
while (res.length() < digits) {
res.insert(0, "0");
}
return res.toString();
return new OTP(otp, digits);
}
}
@@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private int _code;
private int _digits;
public OTP(int code, int digits) {
_code = code;
_digits = digits;
}
public int getCode() {
return _code;
}
public int getDigits() {
return _digits;
}
@NonNull
@Override
public String toString() {
int code = _code % (int) Math.pow(10, _digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(code));
while (res.length() < _digits) {
res.insert(0, "0");
}
return res.toString();
}
public String toSteamString() {
int code = _code;
StringBuilder res = new StringBuilder();
for (int i = 0; i < _digits; i++) {
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
res.append(c);
code /= STEAM_ALPHABET.length();
}
return res.toString();
}
}
@@ -8,13 +8,13 @@ public class TOTP {
private TOTP() {
}
public static String generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
public static String generateOTP(byte[] secret, String algo, int digits, long period)
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
@@ -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<>();
@@ -12,7 +12,7 @@ import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
public class DatabaseFile {
public static final byte VERSION = 1;
@@ -54,8 +54,8 @@ public class DatabaseFile {
try {
String string = obj.toString(4);
return string.getBytes("UTF-8");
} catch (JSONException | UnsupportedEncodingException e) {
return string.getBytes(StandardCharsets.UTF_8);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
@@ -79,9 +79,9 @@ public class DatabaseFile {
public static DatabaseFile fromBytes(byte[] data) throws DatabaseFileException {
try {
JSONObject obj = new JSONObject(new String(data, "UTF-8"));
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return DatabaseFile.fromJson(obj);
} catch (UnsupportedEncodingException | JSONException e) {
} catch (JSONException e) {
throw new DatabaseFileException(e);
}
}
@@ -94,8 +94,8 @@ public class DatabaseFile {
try {
byte[] bytes = Base64.decode((String) _content);
CryptResult result = creds.decrypt(bytes, _header.getParams());
return new JSONObject(new String(result.getData(), "UTF-8"));
} catch (MasterKeyException | JSONException | UnsupportedEncodingException | Base64Exception e) {
return new JSONObject(new String(result.getData(), StandardCharsets.UTF_8));
} catch (MasterKeyException | JSONException | Base64Exception e) {
throw new DatabaseFileException(e);
}
}
@@ -108,12 +108,12 @@ public class DatabaseFile {
public void setContent(JSONObject obj, DatabaseFileCredentials creds) throws DatabaseFileException {
try {
String string = obj.toString(4);
byte[] dbBytes = string.getBytes("UTF-8");
byte[] dbBytes = string.getBytes(StandardCharsets.UTF_8);
CryptResult result = creds.encrypt(dbBytes);
_content = Base64.encode(result.getData());
_header = new Header(creds.getSlots(), result.getParams());
} catch (MasterKeyException | UnsupportedEncodingException | JSONException e) {
} catch (MasterKeyException | JSONException e) {
throw new DatabaseFileException(e);
}
}
@@ -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";
@@ -1,6 +1,6 @@
package com.beemdevelopment.aegis.encoding;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
@@ -19,11 +19,6 @@ public class Base64 {
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
try {
return new String(encoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return new String(encoded, StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,49 @@
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
}
}
@@ -22,12 +22,12 @@ package com.beemdevelopment.aegis.helpers;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import androidx.annotation.RequiresApi;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.mattprecious.swirl.SwirlView;
import com.beemdevelopment.aegis.R;
import androidx.annotation.RequiresApi;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
@@ -3,12 +3,13 @@ package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionHelper {
private PermissionHelper() {
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@@ -1,12 +1,13 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.annotation.ArrayRes;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
import androidx.annotation.ArrayRes;
public class SpinnerHelper {
private SpinnerHelper() {
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<DatabaseEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<DatabaseEntry> {
@Override
public int compare(DatabaseEntry a, DatabaseEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
@@ -1,64 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONObject;
import java.util.List;
import com.beemdevelopment.aegis.db.Database;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseException;
import com.beemdevelopment.aegis.db.DatabaseFile;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseFileException;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class 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 List<DatabaseEntry> convert() throws DatabaseImporterException {
try {
JSONObject obj;
if (_file.isEncrypted() && _creds != null) {
obj = _file.getContent(_creds);
} else {
obj = _file.getContent();
}
Database db = Database.fromJson(obj);
return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -0,0 +1,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,87 +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.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
public class 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(), "UTF-8"));
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = "";
String name = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
} catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -0,0 +1,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,50 +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 = Collections.unmodifiableMap(importers);
}
private Context _context;
protected DatabaseAppImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
public abstract List<DatabaseEntry> 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 List<DatabaseEntry> 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;
List<DatabaseEntry> 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);
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
}
public String getText() {
return _text;
}
}
@@ -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,81 +19,93 @@ 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 List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
public static class State extends DatabaseImporter.State {
private List<XmlEntry> _entries;
try {
for (XmlEntry xmlEntry : _xmlEntries) {
if (xmlEntry.Name.equals("tokenOrder")) {
// TODO: order
JSONArray array = new JSONArray(xmlEntry.Value);
} else {
JSONObject obj = new JSONObject(xmlEntry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
if (type.equals("totp")) {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
} else if (type.equals("hotp")) {
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
} else {
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
}
}
} catch (OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
private State(List<XmlEntry> entries) {
super(false);
_entries = entries;
}
return entries;
}
@Override
public Result convert() {
Result result = new Result();
@Override
public boolean isEncrypted() {
return false;
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;
}
private static DatabaseEntry convertEntry(XmlEntry entry) throws DatabaseImporterEntryException {
try {
JSONObject obj = new JSONObject(entry.Value);
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
OtpInfo info;
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);
}
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);
}
}
}
private static List<XmlEntry> parse(XmlPullParser parser)
@@ -158,4 +176,9 @@ public class FreeOtpFileImporter extends DatabaseFileImporter {
}
}
}
private static class XmlEntry {
String Name;
String Value;
}
}
@@ -1,124 +0,0 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthAppImporter extends DatabaseAppImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@SuppressLint("SdCardPath")
private static final String _filename = "/data/data/com.google.android.apps.authenticator2/databases/databases";
private List<DatabaseEntry> _entries = new ArrayList<>();
public GoogleAuthAppImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (SuFileInputStream in = new SuFileInputStream(new SuFile(_filename))) {
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(in, out);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
if (!cursor.moveToFirst()) {
return;
}
do {
int type = getInt(cursor, "type");
byte[] secret = Base32.decode(getString(cursor, "secret").toCharArray());
OtpInfo info;
switch (type) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, getInt(cursor, "counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name = getString(cursor, "email", "");
String issuer = getString(cursor, "issuer", "");
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
_entries.add(entry);
} while(cursor.moveToNext());
}
} catch (SQLiteException | OtpInfoException | Base32Exception e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@Override
public List<DatabaseEntry> convert() {
return _entries;
}
@Override
public boolean isEncrypted() {
return false;
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
}
@@ -0,0 +1,188 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.topjohnwu.superuser.ShellUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
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 (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(reader.getStream(), out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
List<Entry> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
Entry entry = new Entry(cursor);
entries.add(entry);
} while(cursor.moveToNext());
}
return new State(entries);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
public static class State extends DatabaseImporter.State {
private List<Entry> _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 {
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());
}
}
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
private static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
private static class Entry {
private int _type;
private String _secret;
private String _email;
private String _issuer;
private long _counter;
public Entry(Cursor cursor) {
_type = getInt(cursor, "type");
_secret = getString(cursor, "secret");
_email = getString(cursor, "email", "");
_issuer = getString(cursor, "issuer", "");
_counter = getLong(cursor, "counter");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getEmail() {
return _email;
}
public String getIssuer() {
return _issuer;
}
public long getCounter() {
return _counter;
}
}
}
@@ -0,0 +1,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,6 +1,7 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.HOTP;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import org.json.JSONException;
import org.json.JSONObject;
@@ -9,6 +10,8 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
public static final String ID = "hotp";
private long _counter;
public HotpInfo(byte[] secret, long counter) throws OtpInfoException {
@@ -28,7 +31,8 @@ public class HotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}
@@ -36,7 +40,7 @@ public class HotpInfo extends OtpInfo {
@Override
public String getType() {
return "hotp";
return ID;
}
@Override
@@ -98,13 +98,14 @@ public abstract class OtpInfo implements Serializable {
int digits = obj.getInt("digits");
switch (type) {
case "totp":
int period = obj.getInt("period");
info = new TotpInfo(secret, algo, digits, period);
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "hotp":
long counter = obj.getLong("counter");
info = new HotpInfo(secret, algo, digits, counter);
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, obj.getInt("period"));
break;
case HotpInfo.ID:
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new OtpInfoException("unsupported otp type: " + type);
@@ -126,7 +127,8 @@ public abstract class OtpInfo implements Serializable {
}
OtpInfo info = (OtpInfo) o;
return Arrays.equals(getSecret(), info.getSecret())
return getType().equals(info.getType())
&& Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
}
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
public SteamInfo(byte[] secret) throws OtpInfoException {
super(secret, "SHA1", 5, 30);
}
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
super(secret, algorithm, digits, period);
}
@Override
public String getOtp() {
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toSteamString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return ID;
}
}
@@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
@@ -9,6 +10,8 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
public static final String ID = "totp";
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
@@ -24,7 +27,8 @@ public class TotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
@@ -32,7 +36,7 @@ public class TotpInfo extends OtpInfo {
@Override
public String getType() {
return "totp";
return ID;
}
@Override
@@ -1,28 +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(getPreferences().isDarkModeEnabled());
// 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() {
@@ -33,11 +70,36 @@ public abstract class AegisActivity extends AppCompatActivity {
return _app.getPreferences();
}
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack);
break;
}
}
/**
* 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;
@@ -38,6 +37,7 @@ import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import java.io.ByteArrayInputStream;
@@ -53,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;
@@ -166,7 +166,7 @@ public class EditEntryActivity extends AegisActivity {
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
@@ -189,11 +189,12 @@ public class EditEntryActivity extends AegisActivity {
String type = _spinnerType.getSelectedItem().toString();
switch (type.toLowerCase()) {
case "totp":
case TotpInfo.ID:
case SteamInfo.ID:
_rowCounter.setVisibility(View.GONE);
_rowPeriod.setVisibility(View.VISIBLE);
break;
case "hotp":
case HotpInfo.ID:
_rowPeriod.setVisibility(View.GONE);
_rowCounter.setVisibility(View.VISIBLE);
break;
@@ -215,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 {
@@ -253,15 +255,6 @@ public class EditEntryActivity extends AegisActivity {
}
}
@Override
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
}
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -426,6 +419,14 @@ public class EditEntryActivity extends AegisActivity {
super.onActivityResult(requestCode, resultCode, data);
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
}
private DatabaseEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
@@ -452,16 +453,13 @@ public class EditEntryActivity extends AegisActivity {
OtpInfo info;
try {
switch (type.toLowerCase()) {
case "totp":
int period;
try {
period = Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
info = new TotpInfo(secret, algo, digits, period);
case TotpInfo.ID:
info = new TotpInfo(secret, algo, digits, parsePeriod());
break;
case "hotp":
case SteamInfo.ID:
info = new SteamInfo(secret, algo, digits, parsePeriod());
break;
case HotpInfo.ID:
long counter;
try {
counter = Long.parseLong(_textCounter.getText().toString());
@@ -555,7 +553,7 @@ public class EditEntryActivity extends AegisActivity {
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equals(string)) {
if (res[i].equalsIgnoreCase(string)) {
return i;
}
}
@@ -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,43 +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.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.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 androidx.interpolator.view.animation.FastOutSlowInInterpolator;
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
@@ -48,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,6 +72,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -77,6 +90,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setTapToReveal(getPreferences().isTapToRevealEnabled());
_entryListView.setTapToRevealTime(getPreferences().getTapToRevealTime());
_entryListView.setSortCategory(getPreferences().getCurrentSortCategory(), false);
_entryListView.setViewMode(getPreferences().getCurrentViewMode());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
@@ -84,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
@@ -112,9 +138,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onNewIntent(intent);
setIntent(intent);
if (!doShortcutActions() || _db.isLocked()) {
unlockDatabase(null);
}
doShortcutActions();
}
@Override
@@ -123,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);
@@ -145,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);
}
}
@@ -170,9 +201,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
}
}
@@ -225,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--) {
@@ -252,10 +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);
_entryListView.setGroupFilter(group, true);
}
private void addEntry(DatabaseEntry entry) {
@@ -267,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");
@@ -290,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) {
@@ -308,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
@@ -340,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) {
@@ -399,6 +469,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
updateSortCategoryMenu();
}
return true;
}
@@ -411,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) {
@@ -423,16 +495,34 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
setGroupFilter(group);
}
return super.onOptionsItemSelected(item);
}
}
private void lockDatabase() {
if (_loaded) {
_entryListView.clearEntries();
_db.lock();
_loaded = false;
startAuthActivity();
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
SortCategory sortCategory;
switch (item.getItemId()) {
case R.id.menu_sort_alphabetically:
sortCategory = SortCategory.ISSUER;
break;
case R.id.menu_sort_alphabetically_reverse:
sortCategory = SortCategory.ISSUER_REVERSED;
break;
case R.id.menu_sort_alphabetically_name:
sortCategory = SortCategory.ACCOUNT;
break;
case R.id.menu_sort_alphabetically_name_reverse:
sortCategory = SortCategory.ACCOUNT_REVERSED;
break;
case R.id.menu_sort_custom:
default:
sortCategory = SortCategory.CUSTOM;
break;
}
_entryListView.setSortCategory(sortCategory, true);
getPreferences().setCurrentSortCategory(sortCategory);
}
return super.onOptionsItemSelected(item);
}
}
@@ -464,13 +554,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void loadEntries() {
// load all entries
_entryListView.addEntries(_db.getEntries());
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);
}
@@ -510,38 +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,6 +2,9 @@ package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
@@ -36,4 +39,21 @@ public class PreferencesActivity extends AegisActivity {
outState.putParcelable("result", _fragment.getResult());
super.onSaveInstanceState(outState);
}
@Override
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_Preferences);
break;
}
}
}
@@ -4,44 +4,24 @@ import android.Manifest;
import android.app.Activity;
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.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import com.beemdevelopment.aegis.Preferences;
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.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
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;
@@ -49,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
@@ -57,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;
@@ -65,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;
@@ -86,12 +89,52 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// set the result intent in advance
setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
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 onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
public boolean onPreferenceClick(Preference preference) {
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.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));
dialog.dismiss();
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setPositiveButton(android.R.string.ok, null)
.create());
return true;
}
});
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
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) {
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.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();
})
.setPositiveButton(android.R.string.ok, null)
.create());
return true;
}
});
@@ -189,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();
}
})
@@ -223,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();
}
@@ -258,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;
}
@@ -297,6 +363,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
}
}
@@ -314,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) {
@@ -334,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) {
@@ -397,59 +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 {
List<DatabaseEntry> entries = importer.convert();
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);
Toast.makeText(getActivity(), String.format(Locale.getDefault(), getString(R.string.imported_entries_count), entries.size()), Toast.LENGTH_LONG).show();
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();
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() {
@@ -466,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;
}
@@ -486,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());
}
@@ -516,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,18 +8,19 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
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;
@@ -57,7 +58,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
}
@Override
protected void setPreferredTheme(boolean darkMode) {
protected void setPreferredTheme(Theme theme) {
setTheme(R.style.AppTheme_Fullscreen);
}
@@ -0,0 +1,138 @@
package com.beemdevelopment.aegis.ui;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.getbase.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class SelectEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_select_entries);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
Intent intent = getIntent();
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
for (DatabaseEntry entry : entries) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
}
if (errors.size() > 0) {
showErrorDialog(errors);
}
FloatingActionButton fabMenu = findViewById(R.id.fab);
fabMenu.setOnClickListener(v -> returnSelectedEntries());
_fabScrollHelper = new FabScrollHelper(fabMenu);
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getString(R.string.import_error_dialog, errors.size()))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
.create());
}
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.errors_copied, Toast.LENGTH_SHORT).show();
})
.create());
}
private void returnSelectedEntries() {
List<DatabaseEntry> entries = _adapter.getSelectedEntries();
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
public void onScroll(int dx, int dy) {
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_select_entries, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -2,30 +2,29 @@ package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.ui.views.SlotAdapter;
import javax.crypto.Cipher;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
@@ -0,0 +1,37 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class ImportEntry {
private DatabaseEntry _entry;
private boolean _isChecked = true;
private Listener _listener;
public ImportEntry(DatabaseEntry entry) {
_entry = entry;
}
public void setOnCheckedChangedListener(Listener listener) {
_listener = listener;
}
public DatabaseEntry getDatabaseEntry() {
return _entry;
}
public boolean isChecked() {
return _isChecked;
}
public void setIsChecked(boolean isChecked) {
_isChecked = isChecked;
if (_listener != null) {
_listener.onCheckedChanged(_isChecked);
}
}
public interface Listener {
void onCheckedChanged(boolean value);
}
}
@@ -2,10 +2,11 @@ package com.beemdevelopment.aegis.ui.preferences;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import androidx.annotation.RequiresApi;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import android.util.AttributeSet;
public class SwitchPreference extends SwitchPreferenceCompat {
private Preference.OnPreferenceChangeListener _listener;
@@ -5,8 +5,6 @@ import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -14,23 +12,22 @@ import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import androidx.fragment.app.Fragment;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
@@ -44,7 +41,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher;
private FingerprintManager.CryptoObject _fingerCryptoObj;
private boolean _fingerAuthenticated;
@Override
@@ -74,7 +71,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
public Cipher getFingerCipher() {
return _fingerCipher;
return _fingerCryptoObj.getCipher();
}
public FingerprintSlot getFingerSlot() {
@@ -105,7 +102,7 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
if (_fingerHelper == null) {
@@ -114,11 +111,12 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
}
try {
_fingerCipher = Slot.createEncryptCipher(key);
Cipher cipher = Slot.createEncryptCipher(key);
_fingerCryptoObj = new FingerprintManager.CryptoObject(cipher);
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
_fingerHelper.startListening(_fingerCryptoObj);
break;
default:
throw new RuntimeException();
@@ -3,8 +3,6 @@ package com.beemdevelopment.aegis.ui.slides;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -12,10 +10,12 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.google.android.material.snackbar.Snackbar;
import androidx.fragment.app.Fragment;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
@@ -2,15 +2,13 @@ package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
public class DerivationTask extends ProgressDialogTask<DerivationTask.Params, SecretKey> {
private Callback _cb;
@@ -4,9 +4,11 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Process;
import androidx.annotation.CallSuper;
import com.beemdevelopment.aegis.ui.Dialogs;
import androidx.annotation.CallSuper;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
private ProgressDialog _dialog;
@@ -2,19 +2,17 @@ package com.beemdevelopment.aegis.ui.tasks;
import android.content.Context;
import com.beemdevelopment.aegis.crypto.MasterKey;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.PasswordSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import com.beemdevelopment.aegis.db.slots.SlotList;
import com.beemdevelopment.aegis.db.slots.SlotException;
import com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.db.slots.SlotList;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
private Callback _cb;
@@ -1,10 +1,12 @@
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.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
@@ -13,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;
@@ -27,6 +29,9 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
private boolean _tapToReveal;
private int _tapToRevealTime;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private boolean _isPeriodUniform = true;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
@@ -52,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(true);
}
public void removeEntry(DatabaseEntry entry) {
@@ -83,6 +105,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_shownEntries.remove(position);
notifyItemRemoved(position);
}
checkPeriodUniformity();
}
public void clearEntries() {
@@ -111,6 +135,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
int position = getItemCount() - 1;
notifyItemInserted(position);
}
checkPeriodUniformity();
}
private boolean isEntryFiltered(DatabaseEntry entry) {
@@ -140,17 +166,51 @@ 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);
}
}
// sort the remaining list of entries
Comparator<DatabaseEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
Collections.sort(_shownEntries, comparator);
}
notifyDataSetChanged();
}
public void setViewMode(ViewMode viewMode) {
_viewMode = viewMode;
}
@Override
public void onItemDismiss(int position) {
@@ -182,9 +242,15 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
notifyItemMoved(firstPosition, secondPosition);
}
@Override
public int getItemViewType(int position) {
return _viewMode.getLayoutId();
}
@Override
public EntryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_entry, parent, false);
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
return new EntryHolder(view);
}
@@ -200,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
@@ -244,6 +307,24 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_holders.add(holder);
}
private void checkPeriodUniformity() {
checkPeriodUniformity(false);
}
private void checkPeriodUniformity(boolean force) {
boolean uniform = isPeriodUniform();
if (!force && 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) {
@@ -282,5 +363,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,21 +3,22 @@ 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;
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;
@@ -26,8 +27,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private ImageView _buttonRefresh;
private View _currentView;
private View _entryDivider;
private boolean _hidden;
private int _tapToRevealTime;
@@ -39,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);
@@ -47,6 +46,8 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileDrawable = view.findViewById(R.id.ivTextDrawable);
_buttonRefresh = view.findViewById(R.id.buttonRefresh);
_entryDivider = view.findViewById(R.id.entryDivider);
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
@@ -75,10 +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());
}
setShowProgress(showProgress);
// only show the button if this entry is of type HotpInfo
_buttonRefresh.setVisibility(entry.getInfo() instanceof HotpInfo ? View.VISIBLE : View.GONE);
@@ -116,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();
}
@@ -131,10 +148,18 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
private void updateCode() {
String otp = _entry.getInfo().getOtp();
String text = otp.substring(0, (otp.length() / 2)
+ (otp.length() % 2)) + " "
+ otp.substring(otp.length() / 2);
OtpInfo info = _entry.getInfo();
String text;
if (info instanceof SteamInfo) {
text = info.getOtp();
} else {
String otp = info.getOtp();
text = otp.substring(0, (otp.length() / 2)
+ (otp.length() % 2)) + " "
+ otp.substring(otp.length() / 2);
}
_profileCode.setText(text);
}
@@ -145,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;
}
@@ -1,42 +1,39 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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;
private UiRefresher _refresher;
private RecyclerView.OnScrollChangeListener _onScrollListener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -47,14 +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);
int primaryColorId = getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
// set up the recycler view
_rvKeyProfiles = view.findViewById(R.id.rvKeyProfiles);
_rvKeyProfiles.addOnScrollListener(new RecyclerView.OnScrollListener() {
_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);
@@ -63,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
@@ -88,12 +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, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
if (apply) {
runEntriesAnimation();
}
}
public void setViewMode(ViewMode mode) {
_adapter.setViewMode(mode);
}
public void refresh(boolean hard) {
@@ -103,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;
}
@@ -159,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);
}
@@ -173,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 {
@@ -212,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,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<path
android:fillColor="#FF000000"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

@@ -0,0 +1,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>
@@ -0,0 +1,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/progress">
<clip>
<shape>
<solid android:color="@color/colorAccent"/>
</shape>
</clip>
</item>
</layer-list>
+1 -1
View File
@@ -1,10 +1,10 @@
<?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"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.AuthActivity">
<LinearLayout
android:orientation="vertical"
@@ -2,6 +2,8 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:background="?attr/background"
android:layout_height="match_parent">
<LinearLayout
@@ -36,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"
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.GroupManagerActivity">
<androidx.core.widget.NestedScrollView
+11
View File
@@ -38,6 +38,17 @@
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_qrcode_scan"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_scan_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:fab_title="@string/scan_image"
app:fab_size="mini"
app:fab_colorNormal="@color/background"
app:fab_colorPressed="@color/background"
app:fab_icon="@drawable/ic_add_photo_24px"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_enter"
android:layout_width="wrap_content"
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
android:id="@+id/importEntriesRootView"
tools:context="com.beemdevelopment.aegis.ui.SelectEntriesActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_entries"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:paddingBottom="60dp"
android:clipToPadding="false"
android:scrollbars="vertical"/>
<com.getbase.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:fab_icon="@drawable/ic_check_black_24dp"
app:fab_colorNormal="@color/colorAccent"
app:fab_colorPressed="@color/colorAccent"
app:fab_labelStyle="@style/fab_label_style"
app:fab_labelsPosition="left">
</com.getbase.floatingactionbutton.FloatingActionButton>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -5,6 +5,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/background"
tools:context="com.beemdevelopment.aegis.com.impy.aegis.ui.SlotManagerActivity">
@@ -2,6 +2,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:background="?attr/background"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
@@ -11,7 +12,7 @@
android:id="@+id/copy_button"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:background="?attr/background"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
@@ -31,7 +32,7 @@
android:padding="16dp"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:background="?attr/background"
android:id="@+id/edit_button"
android:orientation="horizontal">
<ImageView
@@ -53,7 +54,7 @@
android:id="@+id/delete_button"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:orientation="horizontal"
android:background="?attr/cardBackground"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingLeft="16dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/ivTextDrawable"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
/>
</RelativeLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_alignParentBottom="false"
android:layout_alignParentTop="false"
android:layout_alignWithParentIfMissing="false"
android:gravity="bottom"
android:id="@+id/relativeLayout"
android:layout_alignParentStart="false"
android:paddingTop="6dp"
android:paddingBottom="8dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/profile_issuer"
android:text="@string/issuer"
android:textColor="?attr/secondaryText"
android:includeFontPadding="false"
android:textSize="13sp"
android:layout_below="@+id/profile_code"
android:layout_alignLeft="@+id/profile_code"
android:layout_alignStart="@+id/profile_code"
android:ellipsize="end"
android:maxLines="1"/>
<TextView
android:id="@+id/profile_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/profile_code"
android:layout_toRightOf="@id/profile_issuer"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/extra_info_text"
android:textSize="13sp"
tools:text=" - AccountName" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:fontFamily="sans-serif-light"
tools:text="012 345"
android:id="@+id/profile_code"
android:includeFontPadding="false"
android:textSize="26sp"
android:textColor="?attr/codePrimaryText"
android:layout_marginLeft="6dp"
android:layout_marginStart="6dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:textStyle="normal|bold"/>
</RelativeLayout>
<ImageView
android:id="@+id/buttonRefresh"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:padding="8dp"
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_refresh_black_24dp"
android:tint="?attr/iconColorPrimary"
android:background="?android:attr/selectableItemBackground" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:padding="0dp"
android:layout_margin="0dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.beemdevelopment.aegis.ui.views.PeriodProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="wrap_content"
android:layout_height="3dp"
android:id="@+id/progressBar"
android:max="1000"
android:layout_weight="1"/>
</LinearLayout>
<View
android:id="@+id/entryDivider"
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/divider" />
</LinearLayout>
@@ -0,0 +1,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>

Some files were not shown because too many files have changed in this diff Show More