Compare commits

...
262 Commits
Author SHA1 Message Date
Alexander Bakker 3506fdaad9 Release v1.1 2020-01-03 12:33:15 +01:00
Michael SchättgenandGitHub 8824b779f5 Merge pull request #273 from alexbakker/fix-notification
Always cancel the persistent notification when the app task is removed
2020-01-03 12:27:17 +01:00
Alexander Bakker 4cab92d615 Always cancel the persistent notification when the app task is removed 2020-01-02 13:05:45 +01:00
Alexander BakkerandGitHub 35a14d3198 Merge pull request #269 from beemdevelopment/update-translations
Update translations
2019-12-27 00:36:10 +01:00
Michael Schättgen e46041ece0 Update translations 2019-12-27 00:29:44 +01:00
Alexander BakkerandGitHub 28ebaf48c8 Merge pull request #268 from beemdevelopment/fix-changelog
Fix changelog
2019-12-26 23:50:58 +01:00
Michael Schättgen fc96e7919a Replace changelog css colors 2019-12-26 23:25:05 +01:00
Michael Schättgen 08b80d2e72 Fix build 2019-12-26 22:17:32 +01:00
Michael SchättgenandGitHub 051cf9370b Merge pull request #257 from alexbakker/action-copy
Add a 'copy' button to the entry action mode menu in MainActivity
2019-12-26 22:14:07 +01:00
Michael SchättgenandGitHub 3d13296890 Merge pull request #267 from alexbakker/biometric-update
Update gradle and dependencies
2019-12-26 22:04:40 +01:00
Michael SchättgenandGitHub 93f32c2903 Merge pull request #266 from alexbakker/db-to-vault
Rename "Database" to "Vault"
2019-12-26 21:54:58 +01:00
Alexander Bakker f168768033 Update gradle and dependencies 2019-12-26 13:23:00 +01:00
Alexander Bakker 5ab4307963 Rename "Database" to "Vault"
We decided on calling the state file the "vault" a while back. This patch makes
the naming consistent across the codebase. I left "DatabaseImporter" classes
alone, because I'm not sure what a better name for those would be.
2019-12-25 19:21:34 +01:00
Alexander Bakker d0baeef064 Release v1.1-beta1 2019-12-24 15:01:39 +01:00
Michael SchättgenandGitHub 44b83c888c Merge pull request #255 from alexbakker/hightlight-and-reveal
Make the behavior of highlighting and revealing entries consistent
2019-12-16 20:35:03 +01:00
Michael SchättgenandGitHub da45fa1969 Merge pull request #240 from alexbakker/biometric
Replace the custom fingerprint auth UI with BiometricPrompt
2019-12-16 17:53:57 +01:00
Alexander BakkerandGitHub 5ad3a10251 Merge pull request #260 from Dissem/master
Fix exception when importing an old export
2019-12-12 12:07:51 +01:00
Christian Basler 9ec94a472f Fix exception when importing an old export 2019-12-11 17:03:22 +01:00
Alexander Bakker 4e4c39eea4 Add a 'copy' button to the entry action mode menu in MainActivity 2019-12-07 18:59:49 +01:00
Alexander Bakker 3be9aecb88 Replace the custom fingerprint auth UI with BiometricPrompt
This patch replaces the usage of the deprecated FingerprintManager API with
BiometricPrompt. This uses the Android X library, so we get the native biometric
prompt on recent versions of Android and a Google-made one on older versions. By
not working with custom prompts for biometric authentication like we do now, we
can be sure that any issues like #70, #81, #237 are not actually our fault.
Here's what it looks like:

![](https://alexbakker.me/u/b2rmf3x0b9.jpeg)

As a nice aside, this also adds support for the new facial recognition as an
authentication method on Pixel 4 phones.

This is still a draft, but early feedback is welcome.
2019-11-27 16:50:33 +01:00
Alexander Bakker a6a5af781e Make the behavior of highlighting and revealing entries consistent
I think this makes the highlight/reveal functionality feel a bit more
consistent, by not allowing multiple entries to be revealed at the same time,
just like you can't have multiple highlighted entries. Here's video of what it
looks like: https://alexbakker.me/u/3a9dhplrj2.mp4.
2019-11-27 16:39:46 +01:00
Michael SchättgenandGitHub a93ced6e34 Merge pull request #254 from alexbakker/fdroid-icon
Update the icon of the F-Droid listing
2019-11-26 22:17:53 +01:00
Alexander BakkerandGitHub 82135c8b9d Merge pull request #238 from michaelschattgen/feature-togglepassword
Add checkbox to toggle visibility in password fields
2019-11-26 21:55:45 +01:00
Michael Schättgen 9f248e0802 Add checkbox to toggle visibility in password fields
Make string translatable
2019-11-26 21:53:29 +01:00
Alexander Bakker 65aa95504b Update the icon of the F-Droid listing 2019-11-26 21:19:14 +01:00
Alexander BakkerandGitHub def7c67614 Merge pull request #239 from michaelschattgen/dialog-text-improvement
Add minor text improvement
2019-10-16 13:18:47 +02:00
Michael Schättgen be3f30e666 Add minor text improvement 2019-10-14 23:18:44 +02:00
Alexander Bakker b920fe4b8c Update README.md 2019-10-14 21:16:25 +02:00
Michael SchättgenandGitHub 4b6b10526d Merge pull request #236 from alexbakker/screenshots
Update screenshots
2019-10-14 21:07:07 +02:00
Michael SchättgenandGitHub 74b2f8f398 Merge pull request #235 from alexbakker/bug-template-device
Ask users for their device model in the bug report template
2019-10-14 21:02:36 +02:00
Michael SchättgenandGitHub 34ef4b3fb4 Merge pull request #232 from alexbakker/chromebook
Explicitly state in AndroidManifest that a camera is not required
2019-10-14 21:02:26 +02:00
Michael SchättgenandGitHub 63b0f6bb99 Merge pull request #220 from alexbakker/highlight-entry
Add the option to highlight entries when tapped
2019-10-14 20:55:53 +02:00
Michael SchättgenandGitHub 36973669ea Merge pull request #219 from alexbakker/new-andotp-format
Add support for the new andOTP backup format
2019-10-14 19:49:06 +02:00
Alexander Bakker 2e301c5389 Update screenshots
This updates the screenshots for F-Droid and the README to the ones shown in our
Play Store listing. I also removed the old icon attribution notice
2019-10-12 13:23:55 +02:00
Alexander Bakker 6a4b0f7bc5 Ask users for their device model in the bug report template 2019-10-12 11:53:14 +02:00
Alexander Bakker 8b063815ab Explicitly state in AndroidManifest that a camera is not required
This hopefully fixes #226. I tested this on a Chromebook emulator, but it let me
install the app even without this patch, so we'll have to wait and see if this
fixes it when we release a new version.
2019-10-09 15:24:03 +02:00
Alexander BakkerandGitHub 75b79d3d4a Merge pull request #203 from michaelschattgen/feature-winauthimport
Add WinAuth importer
2019-09-25 21:54:34 +02:00
Michael Schättgen b61b73685f Add WinAuth importer
Fix WinAuthImporter
2019-09-25 20:36:10 +02:00
Alexander Bakker 864e3bd6f4 Merge pull request #218 from michaelschattgen/feature-translation 2019-09-25 20:00:37 +02:00
Michael SchättgenandGitHub 013e4ae28b Merge pull request #206 from alexbakker/target-29
Target API 29, update gradle and update the dependencies
2019-09-23 22:55:37 +02:00
Alexander Bakker 51a0a16afb Add the option to highlight entries when tapped
This adds an option to highlight tapped entries, so that it's easier to
distinguisch between the one you're trying to enter into a website and the other
ones.

Only one entry can be highlighted at a time. Perhaps it would make sense to
change our tap to reveal functionality to behave the same, so that the two
features are nicely in sync. I can address that in a separate PR if we decide to
do so.
2019-09-21 12:58:35 +02:00
Alexander Bakker a5165e2345 Add support for the new andOTP backup format
This patch adds support for the new backup file format of andOTP. andOTP has
improved their security by switching from SHA-256 to PBKDF2 to derive the key
for encrypted backups.

Glad to see this has been addressed now. Awesome work, @flocke!

See: andOTP/andOTP@d96b037.
2019-09-20 22:45:11 +02:00
Alexander Bakker c41a337791 Fix the Travis CI build by accepting the Android SDK licenses 2019-09-18 21:56:47 +02:00
Alexander Bakker e58dec35c0 Target API 29, update gradle and update the dependencies
This also fixes an issue with the use of the Iconics library where it was
initialized twice. I also removed the dependency to [AndroidX Preference
eXtended](https://github.com/takisoft/preferencex-android), as there don't seem
to be any issues with using the vanilla AndroidX preference library anymore.
2019-09-18 21:56:47 +02:00
Alexander BakkerandGitHub 5e7697039a Merge pull request #214 from michaelschattgen/feature-ungrouped-filter
Add ungrouped filter
2019-09-18 21:55:22 +02:00
Michael Schättgen 45de9cea3a Add Crowdin support 2019-09-17 23:12:27 +02:00
Michael Schättgen 9f075e7bc6 Add ungrouped filter 2019-09-16 23:02:48 +02:00
Michael SchättgenandGitHub a88d1cf11e Merge pull request #211 from alexbakker/show-account-default
Set pref_account_name to true by default
2019-09-16 21:33:15 +02:00
Michael SchättgenandGitHub a6eec88dcb Merge pull request #195 from alexbakker/about-version-git
Add git commit hash and branch information to AboutActivity for debug builds
2019-09-16 20:40:43 +02:00
Michael SchättgenandGitHub 3c4afe36e8 Merge pull request #202 from simonporte/patch-1
Update french translation
2019-09-16 20:36:38 +02:00
simonporteandmdmk 5b71df37ec Update french translation.
Change a single word in english : change log --> changelog
2019-09-16 19:40:40 +02:00
Alexander Bakker d7d3d52a30 Set pref_account_name to true by default
This changes the default settings so that account names are shown by default
2019-09-15 21:04:36 +02:00
Alexander BakkerandGitHub ac222c4946 Merge pull request #201 from michaelschattgen/feature-usernamesearch
Add ability to search in account names
2019-09-14 16:03:45 +01:00
奔跑的梅洛斯andAlexander Bakker 0200287203 Add Simplified Chinese translation (#198) 2019-09-13 18:56:24 +01:00
Alexander Bakker 5677bd4112 Copy the version to the clipboard when tapped in AboutActivity 2019-09-13 19:40:39 +02:00
Alexander Bakker cbe3697d68 Add git commit hash and branch information to AboutActivity for debug builds
This will help us differentiate between custom/debug builds and release builds
of Aegis.
2019-09-13 19:37:47 +02:00
Michael Schättgen 445410fcd7 Add ability to search in account names 2019-09-11 20:51:02 +02:00
Michael Schättgen 1d513441c6 Release v1.0.2 2019-09-10 23:43:10 +02:00
Michael Schättgen 0a31103f91 Disable automatic backups through Google Play
Backups from the Google Play Store had some unintended consequences...
2019-09-10 23:11:22 +02:00
Michael Schättgen 85bf4adc72 Enable and fix search feature again
For some reason we used the old SearchView. Changed it to androidx.appcompat.widget.SearchView and it works on Huawei devices again.
2019-09-10 23:10:11 +02:00
Alexander Bakker cc55a6dacb Release v1.0.1 2019-09-10 00:15:50 +02:00
Alexander Bakker e099e2cacd Add missing null check in onBackPressed for SearchView 2019-09-10 00:14:42 +02:00
Michael Schättgen 658946ff70 Temporarily disable search feature when unavailable
Which isn't working on Huawei devices
2019-09-09 23:51:13 +02:00
Alexander Bakker b6a31ed789 Release v1.0 2019-09-08 14:22:33 +02:00
Alexander BakkerandGitHub 9c272a0d4d Merge pull request #190 from michaelschattgen/feature-about
Add AboutActivity
2019-09-07 12:50:11 +02:00
Michael Schättgen 3e626a37db Add AboutActivity
Add DialogStyles for different themes

Add review fixes

Remove unused usings
2019-09-07 12:49:31 +02:00
Alexander BakkerandGitHub e2150e3823 Merge pull request #194 from michaelschattgen/feature-empty-dataset
Add placeholder for empty recyclerview
2019-09-07 11:55:49 +02:00
Michael Schättgen 6480fef315 Add placeholder for empty recyclerview 2019-09-07 11:48:38 +02:00
Alexander BakkerandGitHub 95df2ceb42 Merge pull request #193 from michaelschattgen/fix-icon-onboarding
Fix app icon in IntroActivity
2019-09-07 10:41:02 +02:00
Michael SchättgenandGitHub 189698dddb Merge pull request #189 from alexbakker/entry-defaults
Set a default value for period and digits for new entries
2019-09-07 00:03:55 +02:00
Michael Schättgen 438d5581aa Fix app icon in IntroActivity 2019-09-06 23:58:04 +02:00
Alexander BakkerandGitHub 7af78b11f5 Merge pull request #192 from michaelschattgen/feature-fixbackpress
Add temporary backpress fix
2019-09-06 22:02:10 +02:00
Michael SchättgenandGitHub 5d68211ce9 Merge pull request #191 from alexbakker/travis-test
Add Travis CI configuration
2019-09-06 21:49:44 +02:00
Michael Schättgen a7e1a88dc8 Add temporary backpress fix 2019-09-06 21:48:17 +02:00
Alexander Bakker 3054032a37 Add Travis CI configuration
This doesn't spin up an emulator yet, but we don't have any tests that require
one anyway
2019-09-05 01:12:30 +02:00
Alexander Bakker 79279630ba Fix all linter errors
Well almost all, just ignore missing translation errors
2019-09-05 01:09:16 +02:00
Alexander Bakker e019fb6db2 Set a default value for period and digits for new entries
This is also in preparation for #24, but I haven't fully figured out how to do
that nicely yet, so that'll come later.
2019-09-04 23:48:30 +02:00
Alexander Bakker 85bdecc573 Fix syntax error in the XML file of the German translation 2019-09-04 23:47:25 +02:00
Andrey SaksonovandAlexander Bakker 21f3d93295 Update Russian localization (#170) 2019-09-03 10:14:08 +02:00
xopezandAlexander Bakker 888e9d0e4a Added German Translation (#188)
* Added German Translation

* Update arrays.xml
2019-09-03 10:10:22 +02:00
Alexander Bakker e6d84e6859 Release v1.0-beta1 2019-08-31 14:05:53 +02:00
Alexander BakkerandGitHub 65371f18d4 Merge pull request #184 from michaelschattgen/feature-setgroupautomatically
Set group on new entries automatically when group filter is active
2019-08-31 13:48:50 +02:00
Michael Schättgen 1b5ab33c25 Add way to pass selected group to edit entry activity 2019-08-31 13:46:18 +02:00
Michael SchättgenandGitHub 396dd0b169 Merge pull request #187 from alexbakker/contributing
Add basic contribution guidelines, issue templates and a guide for ADB
2019-08-29 21:39:52 +02:00
Alexander Bakker eb40369634 Add basic contribution guidelines, issue templates and a guide for ADB 2019-08-29 21:12:17 +02:00
Alexander BakkerandGitHub 0c87f06972 Merge pull request #183 from michaelschattgen/feature-applockdependency
Change auto lock preference visibility behaviour
2019-08-29 11:47:51 +02:00
Michael Schättgen c774796422 Change auto lock preference visibility behaviour 2019-08-28 23:38:12 +02:00
Alexander BakkerandGitHub c12c6ab107 Merge pull request #173 from michaelschattgen/feature-tapholdswipe
Overhaul entry interaction
2019-08-28 23:27:14 +02:00
Michael SchättgenandGitHub e3b9b67fee Merge pull request #181 from alexbakker/fix-152
Check for the possibility of an overflow when parsing OTP period
2019-08-28 23:12:51 +02:00
Michael Schättgen 2407b4b780 Overhaul entry interaction
Add support for different card background colors
Add minor bug fixes
Add minor fixes

Fix minor bugs where action mode kept active
2019-08-28 23:09:52 +02:00
Michael SchättgenandGitHub e962d91c77 Merge pull request #177 from alexbakker/fix-175
Correctly display codes with an uneven number of digits
2019-08-28 22:24:59 +02:00
Michael Schättgen d0afa2f61f Fix app icon 2019-08-28 22:08:06 +02:00
Alexander Bakker 58d13ba9e3 Check for the possibility of an overflow when parsing OTP period
The conversion of the OTP period value to milliseconds may overflow for large
values, causing the result to wrap around to Integer.MIN_VALUE. This
subsequently caused a crash when calling ObjectAnimator.setDuration.
2019-08-28 19:46:47 +02:00
Alexander Bakker 631ca76136 Correctly display codes with an uneven number of digits 2019-08-14 22:12:36 +02:00
Michael SchättgenandGitHub 9cb9d47857 Merge pull request #136 from alexbakker/uuid-map
Introduce UUIDMap for storing objects that are keyed by a UUID
2019-08-04 19:41:27 +02:00
Alexander Bakker 2323d89938 Introduce UUIDMap for storing objects that are keyed by a UUID
This patch introduces the new ``UUIDMap`` type, reducing code duplication and
making UUID lookups faster. We currently already use UUIDs as the identifier for
the ``DatabaseEntry`` and ``Slot`` types, but the way lookups by UUID work are
kind of ugly, as we simply iterate over the list until we find a match. As we're
probably going to have more types like this soon (groups and icons, for
example), I figured it'd be good to abstract this away into a separate type and
make it a map instead of a list.

The only thing that has gotten slower is the ``swap`` method. The internal
``LinkedHashMap`` retains insertion order with a linked list, but does not know
about the position of the values, so we basically have to copy the entire map to
simply swap two values. I don't think it's too big of a deal, because swap
operations still take less than a millisecond even with large vaults, but
suggestions for improving this are welcome.

I had to update gradle and JUnit to be able to use the new ``assertThrows``
assertion method, so this patch includes that as well.
2019-08-03 17:50:25 +02:00
Michael SchättgenandGitHub 6769fefd00 Merge pull request #106 from alexbakker/feature-import-freeotp+
Add support for importing from FreeOTP+
2019-08-03 17:26:30 +02:00
Alexander BakkerandGitHub 5dbf1c7ce2 Merge pull request #172 from michaelschattgen/feature-otpauthuri
Add support for deeplinking otpauth uris
2019-08-03 17:16:13 +02:00
Michael Schättgen 8d3c113475 Add support for deeplinking otpauth uris
Fix bug
2019-08-03 17:15:35 +02:00
Alexander BakkerandGitHub 087bf08d44 Merge pull request #169 from michaelschattgen/feature-showpassword
Add ability to toggle password visibility
2019-08-03 16:13:02 +02:00
Michael SchättgenandGitHub 32b2ce280c Merge pull request #144 from alexbakker/fix-lag-part1
Unregister any listeners when destroying EntryListView
2019-08-02 21:16:42 +02:00
Michael SchättgenandGitHub 37c9b033c5 Merge pull request #141 from alexbakker/pref-lang
Add a language option to the preference menu
2019-08-01 21:17:30 +02:00
Alexander BakkerandGitHub f8d806999d Merge pull request #157 from BM835/master
Updated Russian translation
2019-08-01 11:26:08 +02:00
Michael SchättgenandGitHub 86c4011797 Merge pull request #158 from alexbakker/fix-150
Get external storage read permissions before selecting an image to scan
2019-08-01 00:00:35 +02:00
Michael Schättgen bbc4c1d61c Add ability to toggle password visibility 2019-07-31 23:53:38 +02:00
Alexander BakkerandGitHub 951e908d4b Merge pull request #148 from michaelschattgen/feature-notification
Add notification to warn user about the unlock state
2019-07-31 18:58:06 +02:00
Alexander BakkerandGitHub 536929ff0e Merge pull request #168 from michaelschattgen/feature-icon
Add new icon
2019-07-31 18:54:14 +02:00
Michael Schättgen 1f5a46b883 Add notification to warn user about the unlock state
Clean up code

Add minor changes
2019-07-31 00:37:48 +02:00
Michael Schättgen 4cab47235b Add new icon 2019-07-30 23:30:13 +02:00
Alexander BakkerandGitHub 5524a77736 Merge pull request #163 from wvffle/patch-1
Fix #162
2019-07-30 10:14:18 +02:00
Kasper SewerynandGitHub a3cdfe693d Fix #162 2019-07-11 13:41:38 +02:00
Alexander Bakker 9e4ad1495d Get external storage read permissions before selecting an image to scan
Fixes #150
2019-07-03 18:57:03 +02:00
BM835 914da94c01 Updated Russian translation 2019-07-03 13:14:09 +10:00
Alexander Bakker 950dcce803 Unregister any listeners when destroying EntryListView
We were leaking some resources by not unregistering listeners when destroying
the entry list view. The code refresh loop of the leaked view started running in
a tight infinite loop, which causes a lot of lag in the main activity.
2019-06-25 22:57:17 +02:00
Michael SchättgenandGitHub f82e480c32 Merge pull request #143 from alexbakker/fix-lag-part2
Use glide to load and cache icons
2019-06-25 22:50:31 +02:00
Alexander Bakker 9c5726cbca Use glide to load and cache icons
This patch adds a dependency to glide to handle the loading and caching of
icons. In my testing it eliminated the lag previously experienced in the main
activity when quickly scrolling through a large list of entries. It does add an
extra 1MB to the APK size, but I think that's acceptable for the amount of
complexity it handles for us.
2019-06-25 21:01:50 +02:00
Michael SchättgenandGitHub 550e53f225 Merge pull request #140 from alexbakker/debug-flag-secure
Disable screen security by default for debug builds
2019-06-25 00:15:15 +02:00
Michael SchättgenandGitHub 6e04ca054d Merge pull request #135 from alexbakker/fix-divider
Move entry divider logic to EntryListView
2019-06-25 00:14:19 +02:00
Alexander Bakker b014d95005 Add a language option to the preference menu 2019-06-22 09:58:35 +02:00
Alexander Bakker 0123b98e47 Disable screen security by default for debug builds
I've lost count of how many times I've tried to record a demo video with
screen security enabled.
2019-06-19 14:47:02 +02:00
Alexander Bakker d7093487dd Add support for importing from FreeOTP+
This also includes some other small changes:
- Make FreeOtpImporter more reusable
- Refactor preference file parsing logic into a separate class
- Add support for importing Steam tokens from FreeOTP(+)
- Make FileReader a bit leaner
- Add some missing @Override annotations
2019-06-19 13:09:34 +02:00
Alexander Bakker 02c9a0cb1c Move entry divider logic to EntryListView
This patch makes EntryListView responsible for providing the divider between
entries, instead of setting a margin on every entry like we do now. It also
fixes a couple of miscellaneous issues, like use of the old package name.
2019-06-09 22:41:50 +02:00
Michael Schättgen eb29be587f Fix bug where searchview did not reset properly 2019-06-07 20:57:57 +02:00
Alexander Bakker f64a23d98f Release v0.4.3 2019-06-06 22:27:59 +02:00
Michael SchättgenandGitHub 8bbadb9e57 Merge pull request #130 from alexbakker/fix-import
Limit the amount of entry info passed to SelectEntriesActivitiy
2019-06-06 21:56:28 +02:00
Alexander Bakker 4066cd83cc Limit the amount of entry info passed to SelectEntriesActivitiy
This horrid patch changes the vault import logic to pass an ImportEntry list to
SelectEntriesActivity, instead of a DatabaseEntry list. Previously, a crash
would occur when importing a vault with lots of icons, because the maximum
Parcel size was exceeded.

Storing icons in the vault file was a bad idea.
2019-06-06 01:25:39 +02:00
Alexander BakkerandGitHub cca35bd5e5 Merge pull request #109 from michaelschattgen/feature-entrysearch
Add ability to search for entries in vault
2019-06-02 16:58:53 +02:00
Michael Schättgen 187adfa738 Add ability to search for entries
Fix bug where new entries did not get filtered

Fix IsEntryFiltered
2019-06-02 16:52:32 +02:00
Alexander BakkerandGitHub 246c03dea1 Merge pull request #107 from michaelschattgen/feature-importauthy
Add support for importing from Authy
2019-05-31 10:17:59 +02:00
Alexander BakkerandGitHub e68571a7a6 Merge pull request #108 from michaelschattgen/feature-qrshortcuticon
Add new QR shortcut icon which follows material guidelines
2019-05-31 09:54:36 +02:00
Michael SchättgenandGitHub 128c85fc0c Merge pull request #111 from alexbakker/fix-intro-decrypt
Don't ignore the result of IntroActivity when the vault is locked
2019-05-30 19:42:17 +02:00
Michael Schättgen c75b25c4b6 Add new QR shortcut icon which follows material guidelines
Remove superfluous drawable files
2019-05-30 19:40:47 +02:00
Michael Schättgen 6abcfcea75 Add ability to import from Authy
Fix importer and appImporter lists
2019-05-30 19:35:50 +02:00
Alexander Bakker b300b72626 Don't ignore the result of IntroActivity when the vault is locked
This fixes a bug where AuthActivity would be shown after finishing the intro.
2019-05-30 12:50:40 +02:00
Alexander Bakker b2995955b6 Release v0.4.2 2019-05-29 11:21:52 +02:00
Alexander BakkerandMichael Schättgen 1c9d07a47d Adapt the unlock button size in AuthActivity to the text width 2019-05-28 23:14:15 +02:00
Alexander BakkerandMichael Schättgen 66f99eb4d4 Always launch AuthActivity on lock if MainActivity is resumed 2019-05-28 23:14:04 +02:00
Alexander BakkerandMichael Schättgen ff584a323d Always try to decrypt the password slot first, before the fallback
This makes sure the case where a user set up Aegis v0.4 with a password of over
64 bytes is also covered.
2019-05-28 23:13:17 +02:00
Alexander BakkerandMichael Schättgen 8c658ac930 Use the old encode method for passwords over 64 bytes and repair the slot (#98)
Commit afb9e59711 fixed a bug where the password
encode function would add null bytes to the end of the output. Luckily (I
thought), PBKDF2 produces collisions for inputs with trailing null bytes and
thus scrypt does this as well, so we could safely change that function to remove
the null bytes without any impact. Unfortunately, that doesn't hold up if the
password is over 64 bytes in size. So after that change, the KDF started
producing different keys than before for such passwords and thus some users
could no longer unlock their vault.

This patch addresses the issue by using the old password encode function for
passwords over 64 bytes and repairing the affected password slot.
2019-05-26 23:52:20 +02:00
mdmkandAlexander Bakker 588c1c07df Update french translation (#97)
* Update strings.xml
2019-05-26 21:55:01 +02:00
Alexander BakkerandGitHub f54639c791 Merge pull request #89 from michaelschattgen/feature-ignorespaces
Add possibility to ignore spaces in EditTexts
2019-05-24 10:20:19 +02:00
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
Michael Schättgen 3556cd0e66 Add possibility to ignore spaces in EditTexts 2019-05-21 21:18:01 +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
Alexander Bakker 75c37a454f Release v0.2.1 2019-03-27 00:51:44 +01:00
Michael Schättgen 554d7e17be Fix NullPointerException when deleting tokens 2019-03-27 00:47:39 +01:00
Alexander Bakker 52e4c5cd51 Release v0.2 2019-03-26 23:32:25 +01:00
Alexander Bakker 14b8ba89c8 Fix the last couple of issues with tap-to-reveal 2019-03-26 22:10:29 +01:00
Michael SchättgenandGitHub c9b312a398 Update README.md 2019-03-26 12:51:15 +00:00
Alexander Bakker 9fabd7fe10 Prepare fastlane structure for F-Droid 2019-03-26 09:03:13 +01:00
Alexander Bakker 7f1cea4202 Remove accidentally committed file 2019-03-26 01:03:19 +01:00
Alexander Bakker 29aa81765b Rewrite the HOTP/TOTP code to be more compact and support more digits 2019-03-26 00:59:57 +01:00
Alexander Bakker 066a7447c0 Support an arbitrary number of digits 2019-03-26 00:59:51 +01:00
Alexander Bakker 4cf1a5efe4 Acknowledge libsu in the README 2019-03-26 00:57:27 +01:00
Michael Schättgen a5778e6b38 Show/Hide action button depending on scroll direction 2019-03-26 00:53:32 +01:00
Michael Schättgen cebf15ba44 Add ability to adjust tap to reveal timeout 2019-03-26 00:06:39 +01:00
Michael Schättgen 16bf195ac0 Fix bug in codeIsRevealed when recycling entries 2019-03-25 21:49:50 +01:00
Michael Schättgen d57fc8b79a Add tap to reveal preference to results 2019-03-25 21:40:08 +01:00
Michael Schättgen 0e6828fae1 Add ability to tap to reveal codes 👀 2019-03-25 21:32:29 +01:00
Alexander Bakker ad1d3f04a6 Add support for importing from Google Authenticator
Close #29
2019-03-23 22:43:24 +01:00
Alexander Bakker b33c9383fe Update gradle 2019-03-23 18:16:32 +01:00
260 changed files with 9813 additions and 2774 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: "Bug report"
about: "Create a report to help us fix a bug"
labels: bug
---
<!-- Please read the [bug reports section of the contribution
guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
###### Info
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
###### Steps to reproduce
A detailed list of reproduction steps.
###### What do you expect to happen?
...
###### What happens instead?
...
###### Log
```
If applicable, paste the debug log here.
```
+6
View File
@@ -0,0 +1,6 @@
---
name: "Feature request"
about: "Suggest a new feature for this project"
labels: proposal
---
+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>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<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>
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-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>
+15
View File
@@ -0,0 +1,15 @@
language: android
env:
global:
- ANDROID_HOME=/usr/local/android-sdk
android:
components:
- android-29
- build-tools-29.0.2
before_install:
- mkdir -p $ANDROID_HOME/licenses
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" >> $ANDROID_HOME/licenses/android-sdk-license
script:
- ./gradlew build test
+106
View File
@@ -0,0 +1,106 @@
# Contributing
There are a couple of ways to contribute to Aegis. This document contains some
general guidelines for each type of contribution.
## Pull requests
If you're planning on adding a new feature or making other large changes, please
discuss it with us first through a proposal on GitHub. Discussing your idea with
us first ensures that everyone is on the same page before you start working on
your change. We don't like rejecting pull requests.
## Bug reports
We use GitHub's issue tracker to track bugs. Bug reports must follow [the
template](.github/ISSUE_TEMPLATE/bug.md). If a bug report does not follow the
template and does not contain enough information, it will be closed without a
response. Duplicate bug reports receive the same treatment.
Please consider trying to find the root cause yourself first and perhaps even
send us a patch that fixes the issue! We're happy to help you if you get stuck
along the way.
### Capturing a log with ADB
In some cases, we ask our users to obtain a debug log from their device. This is
typically only necessary if Aegis:
- Is unable to recover from an error and crashes.
- Only shows a generic error to the user, but writes a more detailed one to the
log.
Capturing a log with the Android Debug Bridge (ADB) allows us to see the stack
trace and the exception that occurred.
#### Preparation
Before you can capture a log, you first need to go through a one-time setup
process on your Android device and computer.
##### Prerequisites
- Your Android device.
- A computer with Windows, Mac or Linux.
- A USB cable to connect your Android device to your computer.
##### Setup
__On your Android device__:
1. Navigate to ``Settings -> About``, scroll down and start tapping on the build
number until developer options are enabled.
2. Navigate to ``Settings -> System -> Developer options`` and enable ``USB
debugging``.
These navigation steps may differ slightly across Android versions and ROMs.
__On your computer__:
3. Download and extract the SDK platform tools for Android:
https://developer.android.com/studio/releases/platform-tools.
4. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
5. Execute ``adb devices``.
__On your Android device__:
6. A prompt will appear. Select "Always allow from this computer" and accept the
connection.
#### Capturing a log
__On your Android device__:
1. Start Aegis.
__On your PC__:
2. Start your terminal emulator (If you're on Windows, start PowerShell) and
navigate to the folder where platform-tools was extracted.
3. Start a log capture by executing the following commands.
```
adb logcat -c
adb logcat -f debug.log
```
The logcat command captures the full system log by default, which may expose
some sensitive information. While this information can sometimes help with
finding the root cause of the issue, it is not always necessary. To only
capture the log output of Aegis, replace the last logcat command with the
one below:
```sh
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
```
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
``com.beemdevelopment.aegis.debug``._
__On your Android device__:
4. Reproduce the issue.
__On your PC__:
5. Stop the log capture with Ctrl+C.
6. Attach the ``debug.log`` file to your issue on GitHub.
+85 -16
View File
@@ -2,61 +2,130 @@
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)
- Biometrics (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="assets/screenshot_main.png?raw=true">](assets/screenshot_main.png?raw=true)
[<img width=200 alt="Settings Activity" src="assets/screenshot_settings.png?raw=true">](/assets/screenshot_settings.png?raw=true)
[<img width=200 alt="Edit Activity" src="assets/screenshot_edit.png?raw=true">](/assets/screenshot_edit.png?raw=true)
[<img width=200 alt="Screenshot 1"
src="metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot1.png?raw=true)
[<img width=200 alt="Screenshot 2"
src="metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot2.png?raw=true)
[<img width=200 alt="Screenshot 3"
src="metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true">](/metadata/en-US/images/phoneScreenshots/screenshot3.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_group.png?raw=true">](assets/screenshot_main_group.png?raw=true)
[<img width=200 alt="Main Activity" src="assets/screenshot_main_dark.png?raw=true">](assets/screenshot_main_dark.png?raw=true)
[<img width=200 alt="Screenshot 4"
src="metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot4.png?raw=true)
[<img width=200 alt="Screenshot 5"
src="metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot5.png?raw=true)
[<img width=200 alt="Screenshot 6"
src="metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot6.png?raw=true)
[<img width=200 alt="Screenshot 7"
src="metadata/en-US/images/phoneScreenshots/screenshot7.png?raw=true">](metadata/en-US/images/phoneScreenshots/screenshot7.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
```
### Community
- [aegis-icons](https://github.com/krisu5/aegis-icons)
While we're still working on better icon support in Aegis, krisu5 has started
a third-party repository for icons that you may want to check out.
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
## 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.
+63 -17
View File
@@ -1,27 +1,61 @@
apply plugin: 'com.android.application'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine cmd
standardOutput = stdout
}
return stdout.toString().trim()
}
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "com.beemdevelopment.aegis"
minSdkVersion 19
targetSdkVersion 28
versionCode 2
versionName "0.1.1"
targetSdkVersion 29
versionCode 24
versionName "1.1"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
}
lintOptions {
abortOnError true
disable "MissingTranslation"
}
testOptions {
unitTests.all {
useJUnitPlatform()
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
}
}
}
buildTypes {
debug {
minifyEnabled false
applicationIdSuffix ".debug"
manifestPlaceholders = [title:"AegisDev", iconName:"ic_launcher_debug"]
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
resValue "bool", "pref_secure_screen_default", "false"
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
manifestPlaceholders = [title:"Aegis", iconName:"ic_launcher"]
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
resValue "bool", "pref_secure_screen_default", "true"
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
@@ -29,21 +63,33 @@ android {
}
dependencies {
def libsuVersion = '2.5.1'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.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.apl-devs:appintro:5.1.0'
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'
implementation 'me.dm7.barcodescanner:zxing:1.9'
testImplementation 'junit:junit:4.12'
implementation 'de.hdodenhof:circleimageview:3.0.1'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation 'com.github.bumptech.glide:annotations:4.10.0'
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
implementation("com.github.bumptech.glide:recyclerview-integration:4.9.0") {
transitive = false
}
annotationProcessor 'androidx.annotation:annotation:1.1.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
}
@@ -1,13 +0,0 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
+47 -40
View File
@@ -3,61 +3,68 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.beemdevelopment.aegis">
<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.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<application
android:name="com.beemdevelopment.aegis.AegisApplication"
android:allowBackup="true"
android:name=".AegisApplication"
android:allowBackup="false"
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.AboutActivity"
android:label="@string/title_activity_about"></activity>
<activity
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>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
</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" />
<service android:name=".services.NotificationService" />
</application>
</manifest>
</manifest>
+89
View File
@@ -0,0 +1,89 @@
<html>
<head>
<style type="text/css">
* {
word-wrap: break-word;
}
body {
background-color: %1$s;
color: %2$s;
}
ul {
list-style-position: inside;
padding: 0;
padding-left: 5px;
margin-bottom: 5px;
}
li {
padding-bottom: 8px;
list-style-position: outside;
margin-left: 1em;
}
h4 {
margin-top: 0px;
margin-bottom: 0px;
}
h3 {
margin-bottom: 7px;
padding-top: 7px;
}
</style>
</head>
<body>
<div></div>
<h3>Version 1.1</h3>
<h4>New</h4>
<ul>
<li>Support for other types of biometric authentication (i.e. Pixel 4 face unlock)</li>
<li>Support for importing from WinAuth</li>
<li>Support for Chromebooks</li>
<li>Option to highlight entries when tapped</li>
<li>Filter for ungrouped tokens</li>
<li>Ability to search for token account names</li>
<li>Simplified Chinese translation (thanks RunningMelos!)</li>
<li>Updated translations (thanks to all Crowdin contributers!)</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The behavior of highlighting and revealing entries was inconsistent</li>
<li>The changelog dialog didn't work</li>
<li>The persistent notification was shown even after the app was killed</li>
</ul>
<h3>Version 1.0.3</h3>
<h4>New</h4>
<ul>
<li>Support for andOTP's new backup file format</li>
</ul>
<h3>Version 1.0.2</h3>
<h4>Fixes</h4>
<ul>
<li>Search feature on Huawei devices</li>
</ul>
<h4>Notes</h4>
<ul>
<li>Disabled automatic backups through the Google Play Store</li>
</ul>
<h3>Version 1.0.1</h3>
<h4>Notes</h4>
<ul>
<li>Temporarily disabled search feature on Huawei devices</li>
</ul>
<h3>Version 1.0</h3>
<h4>New</h4>
<ul>
<li>New icon</li>
<li>Overhaul of interaction with the entry list</li>
<li>Persistent notification while the vault is unlocked</li>
<li>Language override option</li>
<li>Support for importing from FreeOTP+</li>
<li>Ability to toggle password visibility during unlock</li>
<li>Support for deeplinking otpauth URIs</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Bad overall performance and high battery usage</li>
<li>Codes with an uneven number of digits are displayed incorrectly</li>
<li>Crash when entering a large value for OTP period</li>
</ul>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

@@ -1,35 +1,64 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
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;
import android.os.Build;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.ui.MainActivity;
import androidx.annotation.RequiresApi;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class AegisApplication extends Application {
private DatabaseManager _manager;
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@Override
public void onCreate() {
super.onCreate();
_manager = new DatabaseManager(this);
_manager = new VaultManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
// listen for SCREEN_OFF events
ScreenOffReceiver receiver = new ScreenOffReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
registerReceiver(receiver, intentFilter);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}
}
public DatabaseManager getDatabaseManager() {
public VaultManager getVaultManager() {
return _manager;
}
@@ -37,6 +66,27 @@ 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();
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
@@ -51,10 +101,37 @@ public class AegisApplication extends Application {
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setIcon(Icon.createWithResource(this, R.drawable.qr_scanner))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
public class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
}
}
}
public interface LockListener {
void onLocked();
}
}
@@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -2,8 +2,12 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Build;
import android.preference.PreferenceManager;
import java.util.Locale;
public class Preferences {
private SharedPreferences _prefs;
@@ -11,27 +15,86 @@ 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);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isSecureScreenEnabled() {
return _prefs.getBoolean("pref_secure_screen", true);
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", false);
return _prefs.getBoolean("pref_account_name", true);
}
public boolean isIntroDone() {
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();
}
public void setTapToRevealTime(int number) {
_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);
}
public Locale getLocale() {
String lang = _prefs.getString("pref_lang", "system");
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Resources.getSystem().getConfiguration().getLocales().get(0);
} else {
return Resources.getSystem().getConfiguration().locale;
}
}
return new Locale(lang);
}
}
@@ -0,0 +1,64 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.vault.VaultEntry;
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<VaultEntry> getComparator() {
Comparator<VaultEntry> 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,44 @@
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;
}
}
/**
* Retrieves the height (in dp) that the divider between entries should have in this view mode.
*/
public float getDividerHeight() {
if (this == ViewMode.COMPACT) {
return 0;
}
return 20;
}
}
@@ -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,18 @@ 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);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);
return bytes;
}
@Deprecated
public static byte[] toBytesOld(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
}
}
@@ -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;
}
@@ -1,189 +1,40 @@
package com.beemdevelopment.aegis.crypto.otp;
/*
* OneTimePasswordAlgorithm.java
* OATH Initiative,
* HOTP one-time password algorithm
*
*/
/* Copyright (C) 2004, OATH. All rights reserved.
*
* License to copy and use this software is granted provided that it
* is identified as the "OATH HOTP Algorithm" in all material
* mentioning or referencing this software or this function.
*
* License is also granted to make and use derivative works provided
* that such works are identified as
* "derived from OATH HOTP algorithm"
* in all material mentioning or referencing the derived work.
*
* OATH (Open AuTHentication) and its members make no
* representations concerning either the merchantability of this
* software or the suitability of this software for any particular
* purpose.
*
* It is provided "as is" without express or implied warranty
* of any kind and OATH AND ITS MEMBERS EXPRESSaLY DISCLAIMS
* ANY WARRANTY OR LIABILITY OF ANY KIND relating to this software.
*
* These notices must be retained in any copies of any part of this
* documentation and/or software.
*/
import java.security.NoSuchAlgorithmException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* This class contains static methods that are used to calculate the
* One-Time Password (OTP) using
* JCE to provide the HMAC-SHA-1.
*
* @author Loren Hart
* @version 1.0
*/
public class HOTP {
private HOTP() {
}
// These are used to calculate the check-sum digits.
// 0 1 2 3 4 5 6 7 8 9
private static final int[] doubleDigits =
{0, 2, 4, 6, 8, 1, 3, 5, 7, 9};
/**
* Calculates the checksum using the credit card algorithm.
* This algorithm has the advantage that it detects any single
* mistyped digit and any single transposition of
* adjacent digits.
*
* @param num the number to calculate the checksum for
* @param digits number of significant places in the number
* @return the checksum of num
*/
public static int calcChecksum(long num, int digits) {
boolean doubleDigit = true;
int total = 0;
while (0 < digits--) {
int digit = (int) (num % 10);
num /= 10;
if (doubleDigit) {
digit = doubleDigits[digit];
}
total += digit;
doubleDigit = !doubleDigit;
}
int result = total % 10;
if (result > 0) {
result = 10 - result;
}
return result;
}
/**
* This method uses the JCE to provide the HMAC-SHA-1
* algorithm.
* HMAC computes a Hashed Message Authentication Code and
* in this case SHA1 is the hash algorithm used.
*
* @param keyBytes the bytes to use for the HMAC-SHA-1 key
* @param text the message or text to be authenticated.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not a valid HMAC-SHA-1 key.
*/
public static byte[] hmac_sha1(byte[] keyBytes, byte[] text)
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
// try {
Mac hmacSha1;
try {
hmacSha1 = Mac.getInstance("HmacSHA1");
} catch (NoSuchAlgorithmException nsae) {
hmacSha1 = Mac.getInstance("HMAC-SHA-1");
}
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmacSha1.init(macKey);
return hmacSha1.doFinal(text);
// } catch (GeneralSecurityException gse) {
// throw new UndeclaredThrowableException(gse);
// }
}
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
/**
* This method generates an OTP value for the given
* set of parameters.
*
* @param secret the shared secret
* @param movingFactor the counter, time, or other value that
* changes on a per use basis.
* @param codeDigits the number of digits in the OTP, not
* including the checksum, if any.
* @param addChecksum a flag that indicates if a checksum digit
* should be appended to the OTP.
* @param truncationOffset the offset into the MAC result to
* begin truncation. If this value is out of
* the range of 0 ... 15, then dynamic
* truncation will be used.
* Dynamic truncation is when the last 4
* bits of the last byte of the MAC are
* used to determine the start offset.
* @return A numeric String in base 10 that includes
* {@link codeDigits} digits plus the optional checksum
* digit if requested.
* @throws NoSuchAlgorithmException if no provider makes
* either HmacSHA1 or HMAC-SHA-1
* digest algorithms available.
* @throws InvalidKeyException The secret provided was not
* a valid HMAC-SHA-1 key.
*/
static public String generateOTP(byte[] secret,
long movingFactor,
int codeDigits,
boolean addChecksum,
int truncationOffset)
throws NoSuchAlgorithmException, InvalidKeyException {
// put movingFactor value into text byte array
String result = null;
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = new byte[8];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
byte[] hash = mac.doFinal(counterBytes);
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
// put selected bytes into result int
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
if ((0 <= truncationOffset) &&
(truncationOffset < (hash.length - 4))) {
offset = truncationOffset;
}
int binary =
((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
if (addChecksum) {
otp = (otp * 10) + calcChecksum(otp, codeDigits);
}
result = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
return result;
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();
}
}
@@ -1,170 +1,21 @@
package com.beemdevelopment.aegis.crypto.otp;
/**
Copyright (c) 2011 IETF Trust and the persons identified as
authors of the code. All rights reserved.
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
Redistribution and use in source and binary forms, with or without
modification, is permitted pursuant to, and subject to the license
terms contained in, the Simplified BSD License set forth in Section
4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
(http://trustee.ietf.org/license-info).
*/
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
/**
* This is an example implementation of the OATH
* TOTP algorithm.
* Visit www.openauthentication.org for more information.
*
* @author Johan Rydell, PortWise, Inc.
*/
public class TOTP {
private TOTP() {
}
/**
* This method uses the JCE to provide the crypto algorithm.
* HMAC computes a Hashed Message Authentication Code with the
* crypto hash algorithm as a parameter.
*
* @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
* HmacSHA512)
* @param keyBytes: the bytes to use for the HMAC key
* @param text: the message or text to be authenticated
*/
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text) {
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey =
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
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);
}
/**
* This method converts a HEX string to Byte[]
*
* @param hex: the HEX string
* @return: a byte array
*/
private static byte[] hexStr2Bytes(String hex) {
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i + 1];
return ret;
}
private static final int[] DIGITS_POWER
// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA1");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP256(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA256");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP512(byte[] key,
String time,
int returnDigits) {
return generateTOTP(key, time, returnDigits, "HmacSHA512");
}
/**
* This method generates a TOTP value for the given
* set of parameters.
*
* @param key: the shared secret, HEX encoded
* @param time: a value that reflects a time
* @param returnDigits: number of digits to return
* @param crypto: the crypto function to use
* @return: a numeric String in base 10 that includes
* {@link truncationDigits} digits
*/
public static String generateTOTP(byte[] key,
String time,
int returnDigits,
String crypto) {
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16)
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] hash = hmac_sha(crypto, key, msg);
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[returnDigits];
result = Integer.toString(otp);
while (result.length() < returnDigits) {
result = "0" + result;
}
return result;
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,77 +0,0 @@
package com.beemdevelopment.aegis.db;
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.util.List;
import java.util.UUID;
public class Database {
private static final int VERSION = 1;
private DatabaseEntryList _entries = new DatabaseEntryList();
public JSONObject toJson() {
try {
JSONArray array = new JSONArray();
for (DatabaseEntry e : _entries) {
array.put(e.toJson());
}
JSONObject obj = new JSONObject();
obj.put("version", VERSION);
obj.put("entries", array);
return obj;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
public static Database fromJson(JSONObject obj) throws DatabaseException {
Database db = new Database();
try {
int ver = obj.getInt("version");
if (ver != VERSION) {
throw new DatabaseException("Unsupported version");
}
JSONArray array = obj.getJSONArray("entries");
for (int i = 0; i < array.length(); i++) {
DatabaseEntry entry = DatabaseEntry.fromJson(array.getJSONObject(i));
db.addEntry(entry);
}
} catch (Base64Exception | OtpInfoException | JSONException e) {
throw new DatabaseException(e);
}
return db;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
}
public void removeEntry(DatabaseEntry entry) {
_entries.remove(entry);
}
public void replaceEntry(DatabaseEntry newEntry) {
_entries.replace(newEntry);
}
public void swapEntries(DatabaseEntry entry1, DatabaseEntry entry2) {
_entries.swap(entry1, entry2);
}
public List<DatabaseEntry> getEntries() {
return _entries.getList();
}
public DatabaseEntry getEntryByUUID(UUID uuid) {
return _entries.getByUUID(uuid);
}
}
@@ -1,62 +0,0 @@
package com.beemdevelopment.aegis.db;
import androidx.annotation.NonNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
public class DatabaseEntryList implements Iterable<DatabaseEntry>, Serializable {
private List<DatabaseEntry> _entries = new ArrayList<>();
@NonNull
@Override
public Iterator<DatabaseEntry> iterator() {
return _entries.iterator();
}
public void add(DatabaseEntry entry) {
if (getByUUID(entry.getUUID()) != null) {
throw new AssertionError("entry found with the same uuid");
}
_entries.add(entry);
}
public void remove(DatabaseEntry entry) {
entry = mustGetByUUID(entry.getUUID());
_entries.remove(entry);
}
public void replace(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = mustGetByUUID(newEntry.getUUID());
_entries.set(_entries.indexOf(oldEntry), newEntry);
}
public void swap(DatabaseEntry entry1, DatabaseEntry entry2) {
Collections.swap(_entries, _entries.indexOf(entry1), _entries.indexOf(entry2));
}
public List<DatabaseEntry> getList() {
return Collections.unmodifiableList(_entries);
}
public DatabaseEntry getByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
}
}
return null;
}
private DatabaseEntry mustGetByUUID(UUID uuid) {
DatabaseEntry entry = getByUUID(uuid);
if (entry == null) {
throw new AssertionError("no entry found with the same uuid");
}
return entry;
}
}
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseException extends Exception {
public DatabaseException(Throwable cause) {
super(cause);
}
public DatabaseException(String message) {
super(message);
}
}
@@ -1,11 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseFileException extends Exception {
public DatabaseFileException(Throwable cause) {
super(cause);
}
public DatabaseFileException(String message) {
super(message);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.db;
public class DatabaseManagerException extends Exception {
public DatabaseManagerException(Throwable cause) {
super(cause);
}
}
@@ -1,20 +0,0 @@
package com.beemdevelopment.aegis.db.slots;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import java.util.UUID;
public class FingerprintSlot extends RawSlot {
public FingerprintSlot() {
super();
}
FingerprintSlot(UUID uuid, byte[] key, CryptParameters keyParams) {
super(uuid, key, keyParams);
}
@Override
public byte getType() {
return TYPE_FINGERPRINT;
}
}
@@ -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,136 @@
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}
@@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricConstants.ERROR_CANCELED
|| errorCode == BiometricConstants.ERROR_USER_CANCELED
|| errorCode == BiometricConstants.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}
@@ -14,9 +14,17 @@ public class EditTextHelper {
}
public static char[] getEditTextChars(EditText text) {
Editable editable = text.getText();
char[] chars = new char[editable.length()];
editable.getChars(0, editable.length(), chars, 0);
return getEditTextChars(text, false);
}
public static char[] getEditTextChars(EditText text, boolean removeSpaces) {
String editTextString = text.getText().toString();
if (removeSpaces) {
editTextString = editTextString.replaceAll("\\s","");
}
char[] chars = new char[editTextString.length()];
editTextString.getChars(0, editTextString.length(), chars, 0);
return chars;
}
@@ -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();
}
}
}
@@ -1,26 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.Manifest;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
public class FingerprintHelper {
private FingerprintHelper() {
}
public static FingerprintManager getManager(Context context) {
if (isSupported() && PermissionHelper.granted(context, Manifest.permission.USE_FINGERPRINT)) {
FingerprintManager manager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
if (manager != null && manager.isHardwareDetected() && manager.hasEnrolledFingerprints()) {
return manager;
}
}
return null;
}
public static boolean isSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}
@@ -1,156 +0,0 @@
// This file was originally taken from https://github.com/googlesamples/android-FingerprintDialog/blob/2feb02945ae220ebd1bc2c2b620a1d43e30daea8/Application/src/main/java/com/example/android/fingerprintdialog/FingerprintUiHelper.java
// It has been modified to suit Aegis' needs
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
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.mattprecious.swirl.SwirlView;
import com.beemdevelopment.aegis.R;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
private static final long ERROR_TIMEOUT_MILLIS = 1600;
private static final long SUCCESS_DELAY_MILLIS = 100;
private final FingerprintManager mFingerprintManager;
private final SwirlView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
private boolean mSelfCancelled;
/**
* Constructor for {@link FingerprintUiHelper}.
*/
public FingerprintUiHelper(FingerprintManager fingerprintManager,
SwirlView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
// The line below prevents the false positive inspection from Android Studio
// noinspection ResourceType
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setState(SwirlView.State.ON);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setState(SwirlView.State.OFF);
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
// ugly hack to keep the fingerprint icon visible while also giving visual feedback of success to the user
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mIcon.setState(SwirlView.State.ON);
}
}, 500);
}
private void showError(CharSequence error) {
mIcon.setState(SwirlView.State.ERROR);
mErrorTextView.setText(error);
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
private Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setState(SwirlView.State.ON);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}
@@ -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,10 +1,15 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final ItemTouchHelperAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
@@ -22,6 +27,10 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(VaultEntry entry) {
_selectedEntry = entry;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
@@ -30,7 +39,15 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
int swipeFlags = 0;
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.getEntryAt(position) != _selectedEntry)
{
dragFlags = 0;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
@@ -56,4 +73,6 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
_positionChanged = false;
}
}
}
@@ -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,23 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Resources;
import android.graphics.Color;
import android.util.TypedValue;
import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
public class ThemeHelper {
private ThemeHelper() {
}
public static int getThemeColor(int attributeId, Resources.Theme currentTheme) {
TypedValue typedValue = new TypedValue();
currentTheme.resolveAttribute(attributeId, typedValue, true);
@ColorInt int color = typedValue.data;
return color;
}
}
@@ -12,6 +12,11 @@ public class UiRefresher {
_handler = new Handler();
}
public void destroy() {
stop();
_listener = null;
}
public void start() {
if (_running) {
return;
@@ -22,15 +27,14 @@ public class UiRefresher {
_handler.postDelayed(new Runnable() {
@Override
public void run() {
if (_running) {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
}
public void stop() {
_handler.removeCallbacksAndMessages(null);
_running = false;
}
@@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}
@@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}
@@ -1,62 +1,110 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.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.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;
import java.io.IOException;
public class AegisImporter extends DatabaseImporter {
private DatabaseFileCredentials _creds;
private DatabaseFile _file;
public AegisImporter(ByteInputStream stream) {
super(stream);
public AegisImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
byte[] bytes = _stream.getBytes();
_file = DatabaseFile.fromBytes(bytes);
} catch (DatabaseFileException e) {
byte[] bytes = reader.readAll();
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (VaultFileException | IOException 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();
public static class EncryptedState extends State {
private VaultFile _file;
private EncryptedState(VaultFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
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 {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
Database db = Database.fromJson(obj);
return db.getEntries();
} catch (DatabaseException | DatabaseFileException e) {
throw new DatabaseImporterException(e);
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | Base64Exception e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
@Override
public boolean isEncrypted() {
return _file.isEncrypted();
}
public void setCredentials(DatabaseFileCredentials creds) {
_creds = creds;
}
public DatabaseFile getFile() {
return _file;
}
}
@@ -1,64 +1,225 @@
package com.beemdevelopment.aegis.importers;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
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.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
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.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
private JSONArray _obj;
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
public AndOtpImporter(ByteInputStream stream) {
super(stream);
private static final int MAX_ITERATIONS = 10000;
public AndOtpImporter(Context context) {
super(context);
}
@Override
public void parse() throws DatabaseImporterException {
protected String getAppPkgName() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
byte[] bytes;
try {
_obj = new JSONArray(new String(_stream.getBytes(), "UTF-8"));
} catch (JSONException e) {
bytes = reader.readAll();
} catch (IOException e) {
throw new DatabaseImporterException(e);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(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);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
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;
}
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
try {
SecretKey key;
int offset = 0;
if (oldFormat) {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// 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));
key = new SecretKeySpec(keyBytes, "AES");
} else {
offset = INT_SIZE + SALT_SIZE;
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1 || iterations > MAX_ITERATIONS) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, offset);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, iterations, KEY_SIZE);
key = factory.generateSecret(spec);
}
// extract nonce and tag
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
})
.create());
}
}
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();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject obj = _obj.getJSONObject(i);
try {
JSONObject obj = _obj.getJSONObject(i);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry 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;
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);
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 name = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
@@ -68,18 +229,10 @@ public class AndOtpImporter extends DatabaseImporter {
name = parts[0];
}
DatabaseEntry entry = new DatabaseEntry(info, name, issuer);
entries.add(entry);
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
} catch (Base32Exception | OtpInfoException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
}
}
@@ -0,0 +1,133 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _pkgName = "com.authy.authy";
public AuthyImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
array = new JSONArray(entry.Value);
}
}
return new State(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONArray _obj;
private State(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName");
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
int digits = entry.getInt("digits");
byte[] secret = Base32.decode(entry.getString("decryptedSecret").toCharArray());
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | Base32Exception e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String seperator = "";
if (info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
seperator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
seperator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + seperator, "");
}
}
private static class AuthyEntryInfo {
String OriginalName;
String AccountType;
String Issuer;
String Name;
}
}
@@ -1,40 +1,74 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.io.SuFile;
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;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.util.ByteInputStream;
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 this list sorted alphabetically
LinkedHashMap<String, Class<? extends DatabaseImporter>> importers = new LinkedHashMap<>();
importers.put("Aegis", AegisImporter.class);
importers.put("andOTP", AndOtpImporter.class);
importers.put("FreeOTP", FreeOtpImporter.class);
_importers = Collections.unmodifiableMap(importers);
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
}
protected ByteInputStream _stream;
protected DatabaseImporter(ByteInputStream stream) {
_stream = stream;
public DatabaseImporter(Context context) {
_context = context;
}
public abstract void parse() throws DatabaseImporterException;
protected Context getContext() {
return _context;
}
public abstract List<DatabaseEntry> convert() throws DatabaseImporterException;
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(getAppPkgName(), getAppSubPath());
}
public abstract boolean isEncrypted();
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
public static DatabaseImporter create(ByteInputStream stream, Class<? extends DatabaseImporter> type) {
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(ByteInputStream.class).newInstance(stream);
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
@@ -42,6 +76,96 @@ public abstract class DatabaseImporter {
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return _importers;
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 UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}
public static class FileReader {
private InputStream _stream;
private boolean _internal;
public FileReader(InputStream stream) {
this(stream, false);
}
public FileReader(InputStream stream, boolean internal) {
_stream = stream;
_internal = internal;
}
public byte[] readAll() throws IOException {
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
return stream.getBytes();
}
}
public InputStream getStream() {
return _stream;
}
/**
* Reports whether this reader reads the internal state of an app.
* @return true if reading from internal file, false if reading from external file
*/
public boolean isInternal() {
return _internal;
}
}
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;
}
}
@@ -1,7 +1,16 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.util.Xml;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -12,86 +21,105 @@ 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 List<XmlEntry> _xmlEntries;
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public FreeOtpImporter(ByteInputStream stream) {
super(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;
}
@Override
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);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public List<DatabaseEntry> convert() throws DatabaseImporterException {
List<DatabaseEntry> entries = new ArrayList<>();
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);
List<JSONObject> entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (!entry.Name.equals("tokenOrder")) {
entries.add(new JSONObject(entry.Value));
}
}
} catch (OtpInfoException | JSONException e) {
return new State(entries);
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
return entries;
}
@Override
public boolean isEncrypted() {
return false;
public static class State extends DatabaseImporter.State {
private List<JSONObject> _entries;
public State(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
OtpInfo info;
switch (type) {
case "totp":
int period = obj.getInt("period");
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
info = new TotpInfo(secret, algo, digits, period);
}
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static List<XmlEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException {
List<XmlEntry> entries = new ArrayList<>();
private static List<JSONObject> parseXml(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
List<JSONObject> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
@@ -104,7 +132,10 @@ public class FreeOtpImporter extends DatabaseImporter {
continue;
}
entries.add(parseEntry(parser));
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
@@ -118,19 +149,21 @@ public class FreeOtpImporter extends DatabaseImporter {
return bytes;
}
private static XmlEntry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
private static JSONObject parseXmlEntry(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException {
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
String value = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
XmlEntry entry = new XmlEntry();
entry.Name = name;
entry.Value = value;
return entry;
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
@@ -0,0 +1,57 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FreeOtpPlusImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.liberty.android.freeotpplus";
public FreeOtpPlusImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
State state;
if (reader.isInternal()) {
state = new FreeOtpImporter(getContext()).read(reader);
} else {
try {
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.State(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
return state;
}
}
@@ -0,0 +1,189 @@
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.vault.VaultEntry;
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;
}
@Override
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 {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry 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 VaultEntry(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,90 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
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();
}
@Override
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 {
VaultEntry entry = convertEntry(_obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static VaultEntry 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 VaultEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -0,0 +1,83 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
return null;
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public Result convert() {
Result result = new Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
VaultEntry vaultEntry = new VaultEntry(info);
vaultEntry.setIssuer(vaultEntry.getName());
vaultEntry.setName("WinAuth");
return vaultEntry;
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -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,11 +10,17 @@ 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) throws OtpInfoException {
public HotpInfo(byte[] secret, long counter) throws OtpInfoException {
super(secret);
setCounter(0);
setCounter(counter);
}
public HotpInfo(byte[] secret) throws OtpInfoException {
this(secret, 0);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
@@ -24,7 +31,8 @@ public class HotpInfo extends OtpInfo {
@Override
public String getOtp() {
try {
return HOTP.generateOTP(getSecret(), getCounter(), getDigits(), false, -1);
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
}
@@ -32,7 +40,7 @@ public class HotpInfo extends OtpInfo {
@Override
public String getType() {
return "hotp";
return ID;
}
@Override
@@ -78,7 +78,8 @@ public abstract class OtpInfo implements Serializable {
}
public static boolean isDigitsValid(int digits) {
return digits == 6 || digits == 8;
// allow a max of 10 digits, as truncation will only extract 31 bits
return digits > 0 && digits <= 10;
}
public void setDigits(int digits) throws OtpInfoException {
@@ -97,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);
@@ -125,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,11 +1,17 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.crypto.otp.OTP;
import com.beemdevelopment.aegis.crypto.otp.TOTP;
import org.json.JSONException;
import org.json.JSONObject;
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 {
@@ -20,13 +26,17 @@ public class TotpInfo extends OtpInfo {
@Override
public String getOtp() {
String time = Long.toHexString(System.currentTimeMillis() / 1000 / getPeriod());
return TOTP.generateTOTP(getSecret(), time, getDigits(), getAlgorithm(true));
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod());
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
return "totp";
return ID;
}
@Override
@@ -45,7 +55,12 @@ public class TotpInfo extends OtpInfo {
}
public static boolean isPeriodValid(int period) {
return period > 0;
if (period <= 0) {
return false;
}
// check for the possibility of an overflow when converting to milliseconds
return period <= Integer.MAX_VALUE / 1000;
}
public void setPeriod(int period) throws OtpInfoException {
@@ -0,0 +1,60 @@
package com.beemdevelopment.aegis.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.beemdevelopment.aegis.R;
public class NotificationService extends Service {
public static final int VAULT_UNLOCKED_ID = 1;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
@Override
public int onStartCommand(Intent intent,int flags, int startId){
super.onStartCommand(intent, flags, startId);
serviceMethod();
return Service.START_STICKY;
}
public void serviceMethod() {
Intent intentAction = new Intent(CODE_LOCK_VAULT_ACTION);
PendingIntent lockDatabaseIntent = PendingIntent.getBroadcast(this, 1, intentAction, 0);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CODE_LOCK_STATUS_ID)
.setSmallIcon(R.drawable.ic_fingerprint_black_24dp)
.setContentTitle(getString(R.string.app_name_full))
.setContentText(getString(R.string.vault_unlocked_state))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setContentIntent(lockDatabaseIntent);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(VAULT_UNLOCKED_ID, builder.build());
}
@Override
public void onDestroy() {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(VAULT_UNLOCKED_ID);
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
stopSelf();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
@@ -0,0 +1,128 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import com.beemdevelopment.aegis.ui.glide.GlideLicense;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
import de.psdev.licensesdialog.LicensesDialog;
public class AboutActivity extends AegisActivity {
private static String GITHUB = "https://github.com/beemdevelopment/Aegis";
private static String WEBSITE_ALEXANDER = "https://alexbakker.me";
private static String GITHUB_MICHAEL = "https://github.com/michaelschattgen";
private static String MAIL_BEEMDEVELOPMENT = "beemdevelopment@gmail.com";
private static String WEBSITE_BEEMDEVELOPMENT = "https://beem.dev/";
private static String PLAYSTORE_BEEMDEVELOPMENT = "https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis";
@Override
protected void onCreate(Bundle savedInstanceState) {
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new IconicsLayoutInflater2(getDelegate()));
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
View btnAppVersion = findViewById(R.id.btn_app_version);
btnAppVersion.setOnClickListener(v -> {
copyToClipboard(getCurrentAppVersion(), R.string.version_copied);
});
View btnGithub = findViewById(R.id.btn_github);
btnGithub.setOnClickListener(v -> openUrl(GITHUB));
View btnAlexander = findViewById(R.id.btn_alexander);
btnAlexander.setOnClickListener(v -> openUrl(WEBSITE_ALEXANDER));
View btnMichael = findViewById(R.id.btn_michael);
btnMichael.setOnClickListener(v -> openUrl(GITHUB_MICHAEL));
View btnMail = findViewById(R.id.btn_email);
btnMail.setOnClickListener(v -> openMail(MAIL_BEEMDEVELOPMENT));
View btnWebsite = findViewById(R.id.btn_website);
btnWebsite.setOnClickListener(v -> openUrl(WEBSITE_BEEMDEVELOPMENT));
View btnRate = findViewById(R.id.btn_rate);
btnRate.setOnClickListener(v -> openUrl(PLAYSTORE_BEEMDEVELOPMENT ));
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
});
}
private static String getCurrentAppVersion() {
if (BuildConfig.DEBUG) {
return String.format("%s-%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.GIT_HASH, BuildConfig.GIT_BRANCH);
}
return BuildConfig.VERSION_NAME;
}
private void openUrl(String url) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW);
browserIntent.setData(Uri.parse(url));
browserIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(browserIntent);
}
private void copyToClipboard(String text, @StringRes int messageId) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData data = ClipData.newPlainText("text/plain", text);
clipboard.setPrimaryClip(data);
Toast.makeText(this, messageId, Toast.LENGTH_SHORT).show();
}
private void openMail(String mailaddress) {
Intent mailIntent = new Intent(Intent.ACTION_SENDTO);
mailIntent.setData(Uri.parse("mailto:" + mailaddress));
mailIntent.putExtra(Intent.EXTRA_EMAIL, mailaddress);
mailIntent.putExtra(Intent.EXTRA_SUBJECT, R.string.app_name_full);
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
}
private void showLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
LicenseResolver.registerLicense(new GlideLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
.show();
}
}
@@ -1,28 +1,83 @@
package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.content.Intent;
import android.content.res.Configuration;
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;
import java.util.Locale;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set the theme and locale before creating the activity
Preferences prefs = getPreferences();
setPreferredTheme(prefs.getCurrentTheme());
setLocale(prefs.getLocale());
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();
}
@Override
protected void onResume() {
super.onResume();
_resumed = true;
}
@Override
protected void onPause() {
super.onPause();
_resumed = false;
}
@CallSuper
@Override
public void onLocked() {
if (isOrphan()) {
setResult(RESULT_CANCELED, null);
finish();
}
}
protected AegisApplication getApp() {
@@ -33,11 +88,58 @@ 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) {
_currentTheme = 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;
}
}
protected void setLocale(Locale locale) {
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
}
/**
* Reports whether this Activity has been resumed. (i.e. onResume was called)
*/
protected boolean isOpen() {
return _resumed;
}
/**
* 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.getVaultManager().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();
}
}
}
protected Theme getCurrentTheme() {
return _currentTheme;
}
}
@@ -1,58 +1,56 @@
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 android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.CancelAction;
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.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
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;
public class AuthActivity extends AegisActivity implements FingerprintUiHelper.Callback, SlotListTask.Callback {
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private FingerprintUiHelper _fingerHelper;
private Cipher _fingerCipher;
private BiometricPrompt.CryptoObject _bioCryptoObj;
private BiometricPrompt _bioPrompt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_auth);
_textPassword = findViewById(R.id.text_password);
LinearLayout boxFingerprint = findViewById(R.id.box_fingerprint);
LinearLayout boxFingerprintInfo = findViewById(R.id.box_fingerprint_info);
TextView textFingerprint = findViewById(R.id.text_fingerprint);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button biometricsButton = findViewById(R.id.button_biometrics);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
@@ -61,23 +59,17 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
return false;
});
SwirlView imgFingerprint = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = findViewById(R.id.img_fingerprint_insert);
imgFingerprint = new SwirlView(this);
insertPoint.addView(imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
// only show the fingerprint controls if the api version is new enough, permission is granted, a scanner is found and a fingerprint slot is found
FingerprintManager manager = FingerprintHelper.getManager(this);
if (manager != null && _slots.has(FingerprintSlot.class)) {
// only show the biometric prompt if the api version is new enough, permission is granted, a scanner is found and a biometric slot is found
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
boolean invalidated = false;
try {
// find a fingerprint slot with an id that matches an alias in the keystore
for (FingerprintSlot slot : _slots.findAll(FingerprintSlot.class)) {
// find a biometric slot with an id that matches an alias in the keystore
for (BiometricSlot slot : _slots.findAll(BiometricSlot.class)) {
String id = slot.getUUID().toString();
KeyStoreHandle handle = new KeyStoreHandle();
if (handle.containsKey(id)) {
@@ -87,30 +79,44 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
invalidated = true;
continue;
}
_fingerCipher = slot.createDecryptCipher(key);
_fingerHelper = new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, this);
boxFingerprint.setVisibility(View.VISIBLE);
Cipher cipher = slot.createDecryptCipher(key);
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (KeyStoreHandleException | SlotException e) {
throw new UndeclaredThrowableException(e);
throw new RuntimeException(e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxFingerprintInfo.setVisibility(View.VISIBLE);
boxBiometricInfo.setVisibility(View.VISIBLE);
}
}
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);
}
});
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
});
if (_bioCryptoObj == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
}
private void showError() {
@@ -118,7 +124,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());
}
@@ -127,53 +133,89 @@ public class AuthActivity extends AegisActivity implements FingerprintUiHelper.C
new SlotListTask<>(type, this, this).execute(params);
}
private void setKey(MasterKey key) {
// send the master key back to the main activity
Intent result = new Intent();
result.putExtra("creds", new DatabaseFileCredentials(key, _slots));
setResult(RESULT_OK, result);
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
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
Intent intent = new Intent();
setResult(RESULT_CANCELED, intent);
finish();
}
}
@Override
public void onResume() {
super.onResume();
if (_fingerHelper != null) {
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
if (_bioPrompt != null) {
showBiometricPrompt();
}
}
public void showBiometricPrompt() {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
_bioPrompt.authenticate(info, _bioCryptoObj);
}
@Override
public void onPause() {
super.onPause();
if (_fingerHelper != null) {
_fingerHelper.stopListening();
if (_bioPrompt != null) {
_bioPrompt.cancelAuthentication();
}
}
@Override
public void onAuthenticated() {
trySlots(FingerprintSlot.class, _fingerCipher);
}
public void onTaskFinished(SlotListTask.Result result) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
}
@Override
public void onError() {
}
@Override
public void onTaskFinished(MasterKey key) {
if (key != null) {
setKey(key);
// send the master key back to the main activity
Intent intent = new Intent();
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
intent.putExtra("repairedSlot", result.isSlotRepaired());
setResult(RESULT_OK, intent);
finish();
} else {
showError();
}
}
private class BiometricPromptListener extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
}
}
@@ -0,0 +1,94 @@
package com.beemdevelopment.aegis.ui;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.WebView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ChangelogDialog extends DialogFragment {
private Theme _themeStyle;
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View customView;
try {
customView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_web_view, null);
} catch (InflateException e) {
e.printStackTrace();
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getString(R.string.webview_error))
.setPositiveButton(android.R.string.ok, null)
.show();
}
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("Changelog")
.setView(customView)
.setPositiveButton(android.R.string.ok, null)
.show();
final WebView webView = customView.findViewById(R.id.web_view);
StringBuilder buf = new StringBuilder();
try (InputStream html = getActivity().getAssets().open("changelog.html")) {
BufferedReader in = new BufferedReader(new InputStreamReader(html, "UTF-8"));
String str;
while ((str = in.readLine()) != null)
buf.append(str);
in.close();
String changelog = buf.toString();
changelog = replaceStylesheet(changelog);
webView.loadData(changelog, "text/html", "UTF-8");
} catch (IOException e) {
webView.loadData("<h1>Unable to load</h1><p>" + e.getLocalizedMessage() + "</p>", "text/html", "UTF-8");
}
return dialog;
}
private String replaceStylesheet(String changelog) {
int backgroundColorResource = _themeStyle == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
String textColor = colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
return String.format(changelog, backgroundColor, textColor);
}
private static String colorToCSS(int color) {
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
}
public ChangelogDialog setTheme(Theme theme) {
_themeStyle = theme;
return this;
}
}
@@ -4,38 +4,33 @@ import android.app.Activity;
import android.app.Dialog;
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.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.TextView;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.mattprecious.swirl.SwirlView;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import android.widget.NumberPicker;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
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 java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
public class Dialogs {
private Dialogs() {
@@ -53,17 +48,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))
@@ -86,6 +70,18 @@ public class Dialogs {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
if (isChecked) {
textPassword.setTransformationMethod(null);
textPassword.clearFocus();
textPasswordConfirm.setEnabled(false);
} else {
textPassword.setTransformationMethod(new PasswordTransformationMethod());
textPasswordConfirm.setEnabled(true);
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
@@ -102,7 +98,7 @@ public class Dialogs {
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
return;
}
@@ -127,10 +123,14 @@ public class Dialogs {
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
}
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);
@@ -138,66 +138,58 @@ 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);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_group)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onTextInputResult(groupName.getText().toString()))
.create();
showSecureDialog(dialog);
}
public static void showFingerprintDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_fingerprint, null);
TextView textFingerprint = view.findViewById(R.id.text_fingerprint);
SwirlView imgFingerprint = view.findViewById(R.id.img_fingerprint);
Cipher cipher;
FingerprintSlot slot;
final AtomicReference<FingerprintUiHelper> helper = new AtomicReference<>();
FingerprintManager manager = FingerprintHelper.getManager(activity);
try {
slot = new FingerprintSlot();
SecretKey key = new KeyStoreHandle().generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
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.register_fingerprint)
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setNegativeButton(android.R.string.cancel, null)
.setOnDismissListener(d -> {
helper.get().stopListening();
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
})
.create();
helper.set(new FingerprintUiHelper(manager, imgFingerprint, textFingerprint, new FingerprintUiHelper.Callback() {
@Override
public void onAuthenticated() {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
}
@Override
public void onError() {
}
}));
helper.get().startListening(new FingerprintManager.CryptoObject(cipher));
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);
numberPicker.setMinValue(3);
numberPicker.setMaxValue(60);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
numberPicker.setWrapSelectorWheel(true);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onNumberInputResult(numberPicker.getValue()))
.create();
showSecureDialog(dialog);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
public interface TextInputListener {
void onTextInputResult(String text);
void onTextInputResult(char[] text);
}
public interface SlotListener {
@@ -4,15 +4,9 @@ import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
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 +24,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.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
@@ -38,31 +34,33 @@ 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 com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.beemdevelopment.aegis.util.Cloner;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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;
private boolean _isNew = false;
private DatabaseEntry _origEntry;
private VaultEntry _origEntry;
private TreeSet<String> _groups;
private boolean _hasCustomIcon = false;
// keep track of icon changes separately as the generated jpeg's are not deterministic
@@ -74,6 +72,7 @@ public class EditEntryActivity extends AegisActivity {
private EditText _textIssuer;
private EditText _textPeriod;
private EditText _textCounter;
private EditText _textDigits;
private EditText _textSecret;
private TableRow _rowPeriod;
@@ -81,7 +80,6 @@ public class EditEntryActivity extends AegisActivity {
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerDigits;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
@@ -101,7 +99,7 @@ public class EditEntryActivity extends AegisActivity {
// retrieve info from the calling activity
Intent intent = getIntent();
_origEntry = (DatabaseEntry) intent.getSerializableExtra("entry");
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
@@ -109,6 +107,8 @@ public class EditEntryActivity extends AegisActivity {
setTitle(R.string.add_new_profile);
}
String selectedGroup = intent.getStringExtra("selectedGroup");
// set up fields
_iconView = findViewById(R.id.profile_drawable);
_kropView = findViewById(R.id.krop_view);
@@ -116,6 +116,7 @@ public class EditEntryActivity extends AegisActivity {
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
_textDigits = findViewById(R.id.text_digits);
_rowPeriod = findViewById(R.id.row_period);
_textCounter = findViewById(R.id.text_counter);
_rowCounter = findViewById(R.id.row_counter);
@@ -124,8 +125,6 @@ public class EditEntryActivity extends AegisActivity {
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerDigits = findViewById(R.id.spinner_digits);
SpinnerHelper.fillSpinner(this, _spinnerDigits, R.array.otp_digits_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
@@ -134,53 +133,49 @@ public class EditEntryActivity extends AegisActivity {
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry != null) {
if (_origEntry.hasIcon()) {
byte[] imageBytes = _origEntry.getIcon();
Bitmap bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_iconView.setImageBitmap(bitmap);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = _origEntry.getInfo().getType();
_spinnerType.setSelection(getStringResourceIndex(R.array.otp_types_array, type.toUpperCase()), false);
String algo = _origEntry.getInfo().getAlgorithm(false);
_spinnerAlgo.setSelection(getStringResourceIndex(R.array.otp_algo_array, algo), false);
String digits = Integer.toString(_origEntry.getInfo().getDigits());
_spinnerDigits.setSelection(getStringResourceIndex(R.array.otp_digits_array, digits), false);
String group = _origEntry.getGroup();
if (group != null) {
int pos = _groups.contains(group) ? _groups.headSet(group).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
if (_origEntry.hasIcon()) {
Glide.with(this)
.asDrawable()
.load(_origEntry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
_hasCustomIcon = true;
} else {
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
}
_textName.setText(_origEntry.getName());
_textIssuer.setText(_origEntry.getIssuer());
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
} else {
throw new RuntimeException();
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
}
String type = _origEntry.getInfo().getType();
_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);
String group = _origEntry.getGroup();
setGroup(group);
// update the icon if the text changed
_textIssuer.addTextChangedListener(_iconChangeListener);
_textName.addTextChangedListener(_iconChangeListener);
@@ -192,11 +187,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;
@@ -218,15 +214,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,18 +250,19 @@ public class EditEntryActivity extends AegisActivity {
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
setGroup(selectedGroup);
}
}
@Override
protected void setPreferredTheme(boolean darkMode) {
if (darkMode) {
setTheme(R.style.AppTheme_Dark);
} else {
setTheme(R.style.AppTheme);
private void setGroup(String groupName) {
if (groupName == null) {
return;
}
}
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
}
private void openAdvancedSettings() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -322,7 +320,7 @@ public class EditEntryActivity extends AegisActivity {
@Override
public void onBackPressed() {
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<DatabaseEntry> entry = new AtomicReference<>();
AtomicReference<VaultEntry> entry = new AtomicReference<>();
try {
entry.set(parseEntry());
@@ -331,7 +329,7 @@ public class EditEntryActivity extends AegisActivity {
}
// close the activity if the entry has not been changed
if (_origEntry != null && !_hasChangedIcon && _origEntry.equals(entry.get())) {
if (!_hasChangedIcon && _origEntry.equals(entry.get())) {
super.onBackPressed();
return;
}
@@ -390,7 +388,7 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void finish(DatabaseEntry entry, boolean delete) {
private void finish(VaultEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entry", entry);
intent.putExtra("delete", delete);
@@ -401,24 +399,30 @@ public class EditEntryActivity extends AegisActivity {
@Override
protected void onActivityResult(int requestCode, final int resultCode, Intent data) {
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null && data.getData() != null) {
Uri inputFile = (data.getData());
InputStream inputStream;
Bitmap bitmap;
try {
inputStream = getContentResolver().openInputStream(inputFile);
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
_kropView.setBitmap(bitmap);
_kropView.setVisibility(View.VISIBLE);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Glide.with(this)
.asBitmap()
.load(data.getData())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
_kropView.setBitmap(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
_iconView.setVisibility(View.GONE);
_kropView.setVisibility(View.VISIBLE);
_saveImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = true;
_hasChangedIcon = true;
@@ -429,7 +433,15 @@ public class EditEntryActivity extends AegisActivity {
super.onActivityResult(requestCode, resultCode, data);
}
private DatabaseEntry parseEntry() throws ParseException {
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriod.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
}
private VaultEntry parseEntry() throws ParseException {
if (_textSecret.length() == 0) {
throw new ParseException("Secret is a required field.");
}
@@ -439,32 +451,28 @@ public class EditEntryActivity extends AegisActivity {
int digits;
try {
digits = Integer.parseInt(_spinnerDigits.getSelectedItem().toString());
digits = Integer.parseInt(_textDigits.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Digits is not an integer.");
}
byte[] secret;
try {
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret));
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
} catch (Base32Exception e) {
throw new ParseException("Secret is not valid base32.");
}
// set otp info
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());
@@ -483,14 +491,8 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("The entered info is incorrect: " + e.getMessage());
}
// set database entry info
DatabaseEntry entry;
if (_origEntry == null) {
entry = new DatabaseEntry(info);
} else {
entry = cloneEntry(_origEntry);
entry.setInfo(info);
}
VaultEntry entry = Cloner.clone(_origEntry);
entry.setInfo(info);
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
@@ -504,8 +506,9 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
drawableToBitmap(_iconView.getDrawable()).compress(Bitmap.CompressFormat.JPEG, 100, stream);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
} else {
@@ -525,7 +528,7 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
DatabaseEntry entry;
VaultEntry entry;
try {
entry = parseEntry();
} catch (ParseException e) {
@@ -558,48 +561,13 @@ 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;
}
}
return -1;
}
private static Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
final int width = !drawable.getBounds().isEmpty() ? drawable
.getBounds().width() : drawable.getIntrinsicWidth();
final int height = !drawable.getBounds().isEmpty() ? drawable
.getBounds().height() : drawable.getIntrinsicHeight();
final Bitmap bitmap = Bitmap.createBitmap(width <= 0 ? 1 : width,
height <= 0 ? 1 : height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private static DatabaseEntry cloneEntry(DatabaseEntry entry) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(entry);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (DatabaseEntry) ois.readObject();
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
@@ -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,36 +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.db.DatabaseFileCredentials;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
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;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
@@ -40,8 +38,8 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private Database _database;
private DatabaseFile _databaseFile;
private Vault _vault;
private VaultFile _vaultFile;
private PasswordSlot _passwordSlot;
private Cipher _passwordCipher;
@@ -66,7 +64,7 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescColor(getResources().getColor(R.color.primary_text_dark));
@@ -84,14 +82,13 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.icon_foreground_white);
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
// create the database and database file
_database = new Database();
_databaseFile = new DatabaseFile();
_vault = new Vault();
_vaultFile = new VaultFile();
}
private void setException(Exception e) {
@@ -131,12 +128,11 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
// generate the master key
DatabaseFileCredentials creds = null;
VaultFileCredentials creds = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
creds = new DatabaseFileCredentials();
creds = new VaultFileCredentials();
}
SlotList slots = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
@@ -151,30 +147,26 @@ public class IntroActivity extends AppIntro2 implements DerivationTask.Callback
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_FINGER) {
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
try {
// encrypt the master key with the fingerprint key
// and add it to the list of slots
FingerprintSlot slot = _authenticatedSlide.getFingerSlot();
Cipher cipher = _authenticatedSlide.getFingerCipher();
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
} catch (SlotException e) {
setException(e);
return;
}
creds.getSlots().add(slot);
}
// finally, save the database
// finally, save the vault
try {
JSONObject obj = _database.toJson();
JSONObject obj = _vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_databaseFile.setContent(obj);
_vaultFile.setContent(obj);
} else {
_databaseFile.setContent(obj, creds);
_vaultFile.setContent(obj, creds);
}
DatabaseManager.save(getApplicationContext(), _databaseFile);
} catch (DatabaseManagerException | DatabaseFileException e) {
VaultManager.save(getApplicationContext(), _vaultFile);
} catch (VaultManagerException | VaultFileException e) {
setException(e);
return;
}
@@ -5,31 +5,52 @@ 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.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
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 androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
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.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
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 com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseManager;
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
@@ -40,24 +61,36 @@ 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;
private static final int CODE_PERM_READ_STORAGE = 1;
private AegisApplication _app;
private DatabaseManager _db;
private VaultManager _vault;
private boolean _loaded;
private String _checkedGroup;
private String _selectedGroup;
private boolean _searchSubmitted;
private VaultEntry _selectedEntry;
private ActionMode _actionMode;
private Menu _menu;
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private FabScrollHelper _fabScrollHelper;
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
_db = _app.getDatabaseManager();
_vault = _app.getVaultManager();
_loaded = false;
// set up the main view
@@ -67,6 +100,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setShowAccountName(getPreferences().isAccountNameVisible());
_entryListView.setSearchAccountName(getPreferences().isSearchAccountNameEnabled());
_entryListView.setHighlightEntry(getPreferences().isEntryHighlightEnabled());
_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);
@@ -74,10 +113,22 @@ 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();
startScanImageActivity();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
}
@Override
protected void onDestroy() {
_entryListView.setListener(null);
super.onDestroy();
}
@Override
@@ -102,9 +153,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
super.onNewIntent(intent);
setIntent(intent);
if (!doShortcutActions() || _db.isLocked()) {
unlockDatabase(null);
}
doShortcutActions();
}
@Override
@@ -113,6 +162,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 && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(resultCode, data);
@@ -135,7 +189,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
break;
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -149,6 +207,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
case CODE_PERM_CAMERA:
startScanActivity();
break;
case CODE_PERM_READ_STORAGE:
startScanImageActivity();
break;
}
}
@@ -158,56 +219,97 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
}
}
private void startEditProfileActivity(int requestCode, DatabaseEntry entry, boolean isNew) {
private void startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (entry != null) {
intent.putExtra("entry", entry);
}
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
intent.putExtra("isNew", isNew);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, requestCode);
}
private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
private void onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
saveVault();
}
}
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
if (data.getBooleanExtra("delete", false)) {
deleteEntry(entry);
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
_db.replaceEntry(entry);
_entryListView.replaceEntry(entry);
saveDatabase();
VaultEntry oldEntry = _vault.replaceEntry(entry);
_entryListView.replaceEntry(oldEntry, entry);
saveVault();
}
}
}
private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
DatabaseEntry entry = (DatabaseEntry) data.getSerializableExtra("entry");
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveDatabase();
saveVault();
}
}
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());
VaultEntry entry = new VaultEntry(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();
}
}
}
@@ -222,30 +324,39 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// if the group no longer exists, switch back to 'All'
TreeSet<String> groups = _db.getGroups();
if (_checkedGroup != null && !groups.contains(_checkedGroup)) {
TreeSet<String> groups = _vault.getGroups();
if (_selectedGroup != null && !groups.contains(_selectedGroup)) {
menu.findItem(R.id.menu_filter_all).setChecked(true);
setGroupFilter(null);
}
for (String group : groups) {
MenuItem item = menu.add(R.id.action_filter_group, Menu.NONE, Menu.NONE, group);
if (group.equals(_checkedGroup)) {
if (group.equals(_selectedGroup)) {
item.setChecked(true);
}
}
if (groups.size() > 0) {
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
}
menu.setGroupCheckable(R.id.action_filter_group, true, true);
}
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_checkedGroup = group;
_entryListView.setGroupFilter(group);
private void updateSortCategoryMenu() {
SortCategory category = getPreferences().getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
private void addEntry(DatabaseEntry entry) {
_db.addEntry(entry);
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_selectedGroup = group;
_entryListView.setGroupFilter(group, true);
}
private void addEntry(VaultEntry entry) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
}
@@ -253,16 +364,22 @@ 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");
unlockDatabase(creds);
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
unlockVault(creds);
}
private void onDecryptResult(int resultCode, Intent intent) {
DatabaseFileCredentials creds = (DatabaseFileCredentials) intent.getSerializableExtra("creds");
unlockDatabase(creds);
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
boolean unlocked = unlockVault(creds);
// save the vault in case a slot was repaired
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
_vault.setCredentials(creds);
saveVault();
}
doShortcutActions();
}
@@ -276,15 +393,23 @@ 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 startScanImageActivity() {
if (!PermissionHelper.request(this, CODE_PERM_READ_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) {
return;
}
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);
}
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 || _vault.isLocked()) {
return;
}
switch (action) {
@@ -294,22 +419,43 @@ 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);
private void handleDeeplink() {
if (_vault.isLocked()) {
return;
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
getIntent().setData(null);
getIntent().setAction(null);
GoogleAuthInfo info = null;
try {
info = GoogleAuthInfo.parseUri(uri);
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
if (info != null) {
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
}
}
@Override
protected void onResume() {
super.onResume();
if (_db.isLocked()) {
// start the intro if the database file doesn't exist
if (!_db.isLoaded() && !_db.fileExists()) {
// the db doesn't exist, start the intro
if (_vault.isLocked()) {
// start the intro if the vault file doesn't exist
if (!_vault.isLoaded() && !_vault.fileExists()) {
// the vault doesn't exist, start the intro
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
@@ -317,7 +463,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
startActivityForResult(intro, CODE_DO_INTRO);
return;
} else {
unlockDatabase(null);
unlockVault(null);
}
} else if (_loaded) {
// update the list of groups in the filter menu
@@ -326,54 +472,41 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
// refresh all codes to prevent showing old ones
_entryListView.refresh(true);
_entryListView.refresh(false);
} else {
loadEntries();
}
handleDeeplink();
updateLockIcon();
doShortcutActions();
}
private BottomSheetDialog createBottomSheet(final DatabaseEntry entry) {
BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(R.layout.bottom_sheet_edit_entry);
dialog.setCancelable(true);
dialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
dialog.show();
@Override
public void onBackPressed() {
if (!_searchView.isIconified() || _searchSubmitted) {
_searchSubmitted = false;
_entryListView.setSearchFilter(null);
dialog.findViewById(R.id.copy_button).setOnClickListener(view -> {
dialog.dismiss();
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
});
collapseSearchView();
setTitle("Aegis");
setGroupFilter(_selectedGroup);
return;
}
dialog.findViewById(R.id.delete_button).setOnClickListener(view -> {
dialog.dismiss();
Dialogs.showDeleteEntryDialog(this, (d, which) -> {
deleteEntry(entry);
// update the filter list if the group no longer exists
if (!_db.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
}
});
});
if (_app.isAutoLockEnabled()) {
_app.lock();
return;
}
dialog.findViewById(R.id.edit_button).setOnClickListener(view -> {
dialog.dismiss();
startEditProfileActivity(CODE_EDIT_ENTRY, entry, false);
});
return dialog;
super.onBackPressed();
}
private void deleteEntry(DatabaseEntry entry) {
_db.removeEntry(entry);
saveDatabase();
private void deleteEntry(VaultEntry entry) {
VaultEntry oldEntry = _vault.removeEntry(entry);
saveVault();
_entryListView.removeEntry(entry);
_entryListView.removeEntry(oldEntry);
}
@Override
@@ -383,19 +516,57 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
updateSortCategoryMenu();
}
MenuItem searchViewMenuItem = menu.findItem(R.id.mi_search);
_searchView = (SearchView) searchViewMenuItem.getActionView();
_searchView.setFocusable(false);
_searchView.setQueryHint(getString(R.string.search));
_searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String s) {
setTitle(getString(R.string.search));
getSupportActionBar().setSubtitle(s);
_searchSubmitted = true;
collapseSearchView();
return false;
}
@Override
public boolean onQueryTextChange(String s) {
if (!_searchSubmitted) {
_entryListView.setSearchFilter(s);
}
return false;
}
});
_searchView.setOnSearchClickListener(v -> {
if (_searchSubmitted) {
_searchSubmitted = false;
_entryListView.setSearchFilter(null);
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
return true;
}
case R.id.action_about: {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
return true;
}
case R.id.action_lock:
lockDatabase();
_app.lock();
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
@@ -407,90 +578,212 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
setGroupFilter(group);
}
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
SortCategory sortCategory;
switch (item.getItemId()) {
case R.id.menu_sort_alphabetically:
sortCategory = SortCategory.ISSUER;
break;
case R.id.menu_sort_alphabetically_reverse:
sortCategory = SortCategory.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);
}
}
private void lockDatabase() {
if (_loaded) {
_entryListView.clearEntries();
_db.lock();
_loaded = false;
startAuthActivity();
}
private void collapseSearchView() {
_searchView.setQuery(null, false);
_searchView.setIconified(true);
}
private void unlockDatabase(DatabaseFileCredentials creds) {
if (_loaded) {
return;
}
private boolean unlockVault(VaultFileCredentials creds) {
try {
if (!_db.isLoaded()) {
_db.load();
if (!_vault.isLoaded()) {
_vault.load();
}
if (_db.isLocked()) {
if (_vault.isLocked()) {
if (creds == null) {
startAuthActivity();
return;
return false;
} else {
_db.unlock(creds);
_vault.unlock(creds);
}
}
} catch (DatabaseManagerException e) {
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
startAuthActivity();
return;
return false;
}
loadEntries();
return true;
}
private void loadEntries() {
// load all entries
_entryListView.addEntries(_db.getEntries());
List<VaultEntry> entries = new ArrayList<VaultEntry>(_vault.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("slots", _vault.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
}
private void saveDatabase() {
private void saveVault() {
try {
_db.save();
} catch (DatabaseManagerException e) {
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
}
}
private void updateLockIcon() {
// hide the lock icon if the database is not unlocked
if (_menu != null && !_db.isLocked()) {
// hide the lock icon if the vault is not unlocked
if (_menu != null && !_vault.isLocked()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_db.isEncryptionEnabled());
item.setVisible(_vault.isEncryptionEnabled());
}
}
@Override
public void onEntryClick(DatabaseEntry entry) {
createBottomSheet(entry).show();
public void onEntryClick(VaultEntry entry) {
if (_selectedEntry != null) {
if (_selectedEntry == entry) {
_actionMode.finish();
}
return;
}
copyEntryCode(entry);
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
_db.swapEntries(entry1, entry2);
public void onLongEntryClick(VaultEntry entry) {
if (_selectedEntry != null) {
return;
}
_selectedEntry = entry;
_entryListView.setActionModeState(true, entry);
_actionMode = this.startSupportActionMode(_actionModeCallbacks);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
saveDatabase();
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_vault.swapEntries(entry1, entry2);
}
@Override
public void onEntryChange(DatabaseEntry entry) {
saveDatabase();
public void onEntryDrop(VaultEntry entry) {
saveVault();
}
@Override
public void onEntryChange(VaultEntry entry) {
saveVault();
}
@Override
public void onScroll(int dx, int dy) {
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public void onLocked() {
if (_actionMode != null) {
_actionMode.finish();
}
_entryListView.clearEntries();
_loaded = false;
if (isOpen()) {
startAuthActivity();
}
super.onLocked();
}
private void copyEntryCode(VaultEntry entry) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
}
private class ActionModeCallbacks implements ActionMode.Callback {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.menu_action_mode, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_copy:
copyEntryCode(_selectedEntry);
mode.finish();
return true;
case R.id.action_edit:
startEditProfileActivity(CODE_EDIT_ENTRY, _selectedEntry, false);
mode.finish();
return true;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(MainActivity.this, (d, which) -> {
deleteEntry(_selectedEntry);
if (_selectedEntry.getGroup() != null) {
if (!_vault.getGroups().contains(_selectedEntry.getGroup())) {
updateGroupFilterMenu();
}
}
mode.finish();
});
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
_entryListView.setActionModeState(false, null);
_selectedEntry = null;
_actionMode = null;
}
}
}
@@ -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,24 +4,52 @@ 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.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.helpers.FingerprintHelper;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
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.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
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.services.NotificationService;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -29,71 +57,108 @@ 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.Objects;
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.db.DatabaseEntry;
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;
public class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
private static final int CODE_IMPORT = 0;
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;
private static final int CODE_PERM_EXPORT = 1;
private Intent _result;
private DatabaseManager _db;
private VaultManager _vault;
// this is used to keep a reference to a database converter
// while the user provides credentials to decrypt it
private DatabaseImporter _importer;
// keep a reference to the type of database converter the user selected
private Class<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private UUIDMap<VaultEntry> _importerEntries;
private SwitchPreference _encryptionPreference;
private SwitchPreference _fingerprintPreference;
private SwitchPreference _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
@Override
public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_db = app.getDatabaseManager();
_vault = app.getVaultManager();
// 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();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference exportPreference = findPreference("pref_import");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
Preference langPreference = findPreference("pref_lang");
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
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();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImport();
@@ -101,8 +166,18 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference importPreference = findPreference("pref_export");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
Preference importAppPreference = findPreference("pref_import_app");
importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImportApp();
return true;
}
});
Preference exportPreference = findPreference("pref_export");
exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onExport();
@@ -129,6 +204,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference searchAccountNamePreference = findPreference("pref_search_names");
searchAccountNamePreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference screenPreference = findPreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@@ -144,23 +237,51 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
tapToRevealTimePreference.setSummary(app.getPreferences().getTapToRevealTime() + " seconds");
tapToRevealTimePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Dialogs.showNumberPickerDialog(getActivity(), new Dialogs.NumberInputListener() {
@Override
public void onNumberInputResult(int number) {
app.getPreferences().setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
_result.putExtra("needsRefresh", true);
}
});
return false;
}
});
_encryptionPreference = (SwitchPreference) findPreference("pref_encryption");
_encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (!_db.isEncryptionEnabled()) {
if (!_vault.isEncryptionEnabled()) {
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();
_vault.disableEncryption();
} catch (VaultManagerException e) {
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();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
})
@@ -171,22 +292,37 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
_fingerprintPreference = (SwitchPreference) findPreference("pref_fingerprint");
_fingerprintPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
_biometricsPreference = (SwitchPreference) findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
DatabaseFileCredentials creds = _db.getCredentials();
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(FingerprintSlot.class)) {
Dialogs.showFingerprintDialog(getActivity(), new RegisterFingerprintListener());
if (!slots.has(BiometricSlot.class)) {
if (BiometricsHelper.isAvailable(getContext())) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
} else {
// remove the fingerprint slot
FingerprintSlot slot = slots.find(FingerprintSlot.class);
// remove the biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
_db.setCredentials(creds);
_vault.setCredentials(creds);
saveDatabase();
// remove the KeyStore key
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.deleteKey(slot.getUUID().toString());
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
saveVault();
updateEncryptionPreferences();
}
@@ -205,7 +341,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", _db.getCredentials());
intent.putExtra("creds", _vault.getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
}
@@ -216,18 +352,25 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(_db.getGroups()));
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
}
});
_autoLockPreference = findPreference("pref_auto_lock");
}
@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;
}
@@ -260,6 +403,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
}
}
@@ -278,10 +424,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[importers.size()]);
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) {
@@ -296,25 +442,98 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
.create());
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importer = null;
return;
private void onImportApp() {
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getAppImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_application)
.setSingleChoiceItems(names, 0, null)
.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 (SuFileInputStream stream = new SuFileInputStream(file)) {
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream, true);
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();
}
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
((AegisImporter)_importer).setCredentials(creds);
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
try {
importDatabase(_importer);
DatabaseImporter.State state = importer.read(reader);
if (state.isEncrypted()) {
// temporary special case for encrypted Aegis vaults
if (state instanceof AegisImporter.EncryptedState) {
_importerState = state;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
intent.putExtra("cancelAction", CancelAction.CLOSE);
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();
}
}
_importer = null;
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importerState = null;
return;
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
DatabaseImporter.State state;
try {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (VaultFileException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
return;
}
importDatabase(state);
_importerState = null;
}
private void onImportResult(int resultCode, Intent data) {
@@ -323,59 +542,39 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
ByteInputStream stream;
try (InputStream fileStream = getActivity().getContentResolver().openInputStream(uri)) {
stream = ByteInputStream.create(fileStream);
try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) {
DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType);
DatabaseImporter.FileReader reader = new DatabaseImporter.FileReader(stream);
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 {
DatabaseImporter importer = DatabaseImporter.create(stream, _importerType);
importer.parse();
// special case to decrypt encrypted aegis databases
if (importer.isEncrypted() && importer instanceof AegisImporter) {
_importer = importer;
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter)_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();
_importerEntries = result.getEntries();
List<ImportEntry> entries = new ArrayList<>();
for (VaultEntry entry : _importerEntries) {
entries.add(new ImportEntry(entry));
}
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) result.getErrors());
startActivityForResult(intent, CODE_SELECT_ENTRIES);
}
private void onExport() {
@@ -386,24 +585,24 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
// TODO: create a custom layout to show a message AND a checkbox
final AtomicReference<Boolean> checked = new AtomicReference<>(true);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle("Export the database")
.setTitle(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename;
try {
filename = _db.export(checked.get());
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.exporting_database_error), Toast.LENGTH_SHORT).show();
filename = _vault.export(checked.get());
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
// make sure the new file is visible
MediaScannerConnection.scanFile(getActivity(), new String[]{filename}, null, null);
Toast.makeText(getActivity(), getString(R.string.export_database_location) + filename, Toast.LENGTH_SHORT).show();
Toast.makeText(getActivity(), getString(R.string.export_vault_location) + filename, Toast.LENGTH_SHORT).show();
})
.setNegativeButton(android.R.string.cancel, null);
if (_db.isEncryptionEnabled()) {
final String[] items = {"Keep the database encrypted"};
if (_vault.isEncryptionEnabled()) {
final String[] items = {"Keep the vault encrypted"};
final boolean[] checkedItems = {true};
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
@@ -412,7 +611,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
});
} else {
builder.setMessage(getString(R.string.export_warning));
builder.setMessage(R.string.export_warning);
}
Dialogs.showSecureDialog(builder.create());
}
@@ -422,9 +621,9 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
DatabaseFileCredentials creds = (DatabaseFileCredentials) data.getSerializableExtra("creds");
_db.setCredentials(creds);
saveDatabase();
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
_vault.setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
@@ -435,18 +634,46 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (DatabaseEntry entry : _db.getEntries()) {
for (VaultEntry entry : _vault.getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
}
private boolean saveDatabase() {
private void onSelectEntriesResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
List<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) data.getSerializableExtra("entries");
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID());
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_vault.isEntryDuplicate(savedEntry)) {
savedEntry.resetUUID();
}
_vault.addEntry(savedEntry);
}
_importerEntries = null;
if (!saveVault()) {
return;
}
String toastMessage = getResources().getString(R.string.imported_entries_count, selectedEntries.size());
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
_result.putExtra("needsRecreate", true);
}
private boolean saveVault() {
try {
_db.save();
} catch (DatabaseManagerException e) {
Toast.makeText(getActivity(), getString(R.string.saving_error), Toast.LENGTH_LONG).show();
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
return false;
}
@@ -454,25 +681,27 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
private void updateEncryptionPreferences() {
boolean encrypted = _db.isEncryptionEnabled();
boolean encrypted = _vault.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_fingerprintPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = _db.getCredentials().getSlots();
SlotList slots = _vault.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiFinger = slots.findAll(FingerprintSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiFinger;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_fingerprintPreference.setEnabled(FingerprintHelper.getManager(getContext()) != null && !multiFinger);
_fingerprintPreference.setChecked(slots.has(FingerprintSlot.class), true);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
} else {
_setPasswordPreference.setEnabled(false);
_fingerprintPreference.setEnabled(false);
_fingerprintPreference.setChecked(false, true);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
}
}
@@ -480,7 +709,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
try {
@@ -498,8 +727,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
return;
}
_db.setCredentials(creds);
saveDatabase();
_vault.setCredentials(creds);
saveVault();
}
@Override
@@ -509,46 +738,46 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private class RegisterFingerprintListener implements Dialogs.SlotListener {
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = _db.getCredentials();
SlotList slots = creds.getSlots();
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
VaultFileCredentials creds = _vault.getCredentials();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
onSlotInitializationFailed(0, e.toString());
return;
}
creds.getSlots().add(slot);
_vault.setCredentials(creds);
slots.add(slot);
_db.setCredentials(creds);
saveDatabase();
saveVault();
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
Toast.makeText(getActivity(), getString(R.string.encryption_enable_fingerprint_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
DatabaseFileCredentials creds = new DatabaseFileCredentials();
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_db.enableEncryption(creds);
} catch (DatabaseManagerException | SlotException e) {
_vault.enableEncryption(creds);
} catch (VaultManagerException | SlotException e) {
onException(e);
return;
}
getActivity().startService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
@@ -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.vault.VaultEntry;
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);
}
@@ -108,7 +109,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
try {
// parse google auth uri
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
DatabaseEntry entry = new DatabaseEntry(info);
VaultEntry entry = new VaultEntry(info);
Intent intent = new Intent();
intent.putExtra("entry", entry);
@@ -0,0 +1,136 @@
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.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<ImportEntry> entries = (ArrayList<ImportEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
for (ImportEntry entry : entries) {
_adapter.addEntry(entry);
}
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<ImportEntry> entries = _adapter.getCheckedEntries();
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<ImportEntry>) 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,33 +2,35 @@ 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 androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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.helpers.FingerprintHelper;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
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;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener, Dialogs.SlotListener {
private DatabaseFileCredentials _creds;
public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Listener {
private VaultFileCredentials _creds;
private SlotAdapter _adapter;
private boolean _edited;
@@ -43,12 +45,19 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
findViewById(R.id.button_add_fingerprint).setOnClickListener(view -> {
Dialogs.showFingerprintDialog(this ,this);
findViewById(R.id.button_add_biometric).setOnClickListener(view -> {
if (BiometricsHelper.isAvailable(this)) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SlotManagerActivity.this, new RegisterBiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.add_biometric_slot))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
});
findViewById(R.id.button_add_password).setOnClickListener(view -> {
Dialogs.showSetPasswordDialog(this, this);
Dialogs.showSetPasswordDialog(this, new PasswordListener());
});
// set up the recycler view
@@ -60,22 +69,22 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
slotsView.setNestedScrollingEnabled(false);
// load the slots and masterKey
_creds = (DatabaseFileCredentials) getIntent().getSerializableExtra("creds");
_creds = (VaultFileCredentials) getIntent().getSerializableExtra("creds");
for (Slot slot : _creds.getSlots()) {
_adapter.addSlot(slot);
}
updateFingerprintButton();
updateBiometricsButton();
}
private void updateFingerprintButton() {
// only show the fingerprint option if we can get an instance of the fingerprint manager
private void updateBiometricsButton() {
// only show the biometrics option if we can get an instance of the biometrics manager
// and if none of the slots in the collection has a matching alias in the keystore
int visibility = View.VISIBLE;
if (FingerprintHelper.getManager(this) != null) {
if (BiometricsHelper.isAvailable(this)) {
try {
KeyStoreHandle keyStore = new KeyStoreHandle();
for (FingerprintSlot slot : _creds.getSlots().findAll(FingerprintSlot.class)) {
for (BiometricSlot slot : _creds.getSlots().findAll(BiometricSlot.class)) {
if (keyStore.containsKey(slot.getUUID().toString())) {
visibility = View.GONE;
break;
@@ -87,7 +96,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
} else {
visibility = View.GONE;
}
findViewById(R.id.button_add_fingerprint).setVisibility(visibility);
findViewById(R.id.button_add_biometric).setVisibility(visibility);
}
private void onSave() {
@@ -163,29 +172,60 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
slots.remove(slot);
_adapter.removeSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
private void addSlot(Slot slot) {
_creds.getSlots().add(slot);
_adapter.addSlot(slot);
_edited = true;
updateFingerprintButton();
updateBiometricsButton();
}
@Override
public void onException(Exception e) {
Toast.makeText(this, getString(R.string.adding_new_slot_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
private void showSlotError(String error) {
Toast.makeText(SlotManagerActivity.this, getString(R.string.adding_new_slot_error) + error, Toast.LENGTH_SHORT).show();
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
addSlot(slot);
} catch (SlotException e) {
onSlotInitializationFailed(0, e.toString());
}
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
showSlotError(errString.toString());
}
}
}
private class PasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
try {
slot.setKey(_creds.getKey(), cipher);
} catch (SlotException e) {
onException(e);
return;
}
addSlot(slot);
}
@Override
public void onException(Exception e) {
showSlotError(e.toString());
}
}
}
@@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import java.nio.ByteBuffer;
@GlideModule
public class AegisGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
}
}
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class GlideLicense extends License {
@Override
public String getName() {
return "Glide License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.glide_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://github.com/bumptech/glide/blob/master/LICENSE";
}
}
@@ -0,0 +1,76 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
@Override
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
}
@Override
public boolean handles(@NonNull VaultEntry model) {
return true;
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private VaultEntry _model;
private Fetcher(VaultEntry model) {
_model = model;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
byte[] bytes = _model.getIcon();
ByteBuffer buf = ByteBuffer.wrap(bytes);
callback.onDataReady(buf);
}
@Override
public void cleanup() {
}
@Override
public void cancel() {
}
@NonNull
@Override
public Class<ByteBuffer> getDataClass() {
return ByteBuffer.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.MEMORY_CACHE;
}
}
public static class Factory implements ModelLoaderFactory<VaultEntry, ByteBuffer> {
@NonNull
@Override
public ModelLoader<VaultEntry, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new IconLoader();
}
@Override
public void teardown() {
}
}
}
@@ -0,0 +1,31 @@
package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Key;
import java.security.MessageDigest;
import java.util.UUID;
public class UUIDKey implements Key {
private UUID _uuid;
public UUIDKey(UUID uuid) {
_uuid = uuid;
}
@Override
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
messageDigest.update(_uuid.toString().getBytes(CHARSET));
}
@Override
public boolean equals(Object o) {
return _uuid.equals(o);
}
@Override
public int hashCode() {
return _uuid.hashCode();
}
}
@@ -0,0 +1,53 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.io.Serializable;
import java.util.UUID;
public class ImportEntry implements Serializable {
private UUID _uuid;
private String _name;
private String _issuer;
private transient Listener _listener;
private boolean _isChecked = true;
public ImportEntry(VaultEntry entry) {
_uuid = entry.getUUID();
_name = entry.getName();
_issuer = entry.getIssuer();
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public String getIssuer() {
return _issuer;
}
public void setOnCheckedChangedListener(Listener listener) {
_listener = listener;
}
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;
@@ -1,66 +1,58 @@
package com.beemdevelopment.aegis.ui.slides;
import android.content.Context;
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.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.FingerprintUiHelper;
import com.mattprecious.swirl.SwirlView;
import java.lang.reflect.UndeclaredThrowableException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.github.paolorotolo.appintro.ISlideSelectionListener;
import com.google.android.material.snackbar.Snackbar;
import com.beemdevelopment.aegis.db.slots.FingerprintSlot;
import com.beemdevelopment.aegis.db.slots.Slot;
import javax.crypto.Cipher;
public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiHelper.Callback, ISlidePolicy, ISlideSelectionListener {
public class CustomAuthenticatedSlide extends Fragment implements ISlidePolicy, ISlideSelectionListener {
private int _cryptType;
private EditText _textPassword;
private EditText _textPasswordConfirm;
private CheckBox _checkPasswordVisibility;
private int _bgColor;
private LinearLayout _boxFingerprint;
private SwirlView _imgFingerprint;
private TextView _textFingerprint;
private FingerprintUiHelper _fingerHelper;
private KeyStoreHandle _storeHandle;
private FingerprintSlot _fingerSlot;
private Cipher _fingerCipher;
private boolean _fingerAuthenticated;
private BiometricSlot _bioSlot;
private Cipher _bioCipher;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_authenticated_slide, container, false);
_textPassword = view.findViewById(R.id.text_password);
_textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
_boxFingerprint = view.findViewById(R.id.box_fingerprint);
_checkPasswordVisibility = view.findViewById(R.id.check_toggle_visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ViewGroup insertPoint = view.findViewById(R.id.img_fingerprint_insert);
_imgFingerprint = new SwirlView(getContext());
insertPoint.addView(_imgFingerprint, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
_checkPasswordVisibility.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
_textPassword.setTransformationMethod(null);
_textPassword.clearFocus();
_textPasswordConfirm.setEnabled(false);
} else {
_textPassword.setTransformationMethod(new PasswordTransformationMethod());
_textPasswordConfirm.setEnabled(true);
}
});
_textFingerprint = view.findViewById(R.id.text_fingerprint);
view.findViewById(R.id.main).setBackgroundColor(_bgColor);
return view;
}
@@ -69,113 +61,96 @@ public class CustomAuthenticatedSlide extends Fragment implements FingerprintUiH
return _cryptType;
}
public BiometricSlot getBiometricSlot() {
return _bioSlot;
}
public Cipher getBiometriCipher() {
return _bioCipher;
}
public char[] getPassword() {
return EditTextHelper.getEditTextChars(_textPassword);
}
public Cipher getFingerCipher() {
return _fingerCipher;
}
public FingerprintSlot getFingerSlot() {
return _fingerSlot;
}
public void setBgColor(int color) {
_bgColor = color;
}
public void showBiometricPrompt() {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(this, new BiometricsListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.set_up_biometric))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
initializer.authenticate(info);
}
@Override
public void onSlideSelected() {
Intent intent = getActivity().getIntent();
_cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
switch(_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
break;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
_boxFingerprint.setVisibility(View.VISIBLE);
SecretKey key;
try {
if (_storeHandle == null) {
_storeHandle = new KeyStoreHandle();
_fingerSlot = new FingerprintSlot();
}
key = _storeHandle.generateKey(_fingerSlot.getUUID().toString());
} catch (KeyStoreHandleException e) {
throw new UndeclaredThrowableException(e);
}
if (_fingerHelper == null) {
FingerprintManager fingerManager = (FingerprintManager) getContext().getSystemService(Context.FINGERPRINT_SERVICE);
_fingerHelper = new FingerprintUiHelper(fingerManager, _imgFingerprint, _textFingerprint, this);
}
try {
_fingerCipher = Slot.createEncryptCipher(key);
} catch (Exception e) {
throw new UndeclaredThrowableException(e);
}
_fingerHelper.startListening(new FingerprintManager.CryptoObject(_fingerCipher));
break;
default:
throw new RuntimeException();
if (_cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
showBiometricPrompt();
}
}
@Override
public void onSlideDeselected() {
if (_fingerHelper != null) {
_fingerAuthenticated = false;
_boxFingerprint.setVisibility(View.INVISIBLE);
_fingerHelper.stopListening();
}
}
@Override
public boolean isPolicyRespected() {
switch(_cryptType) {
switch (_cryptType) {
case CustomAuthenticationSlide.CRYPT_TYPE_NONE:
return true;
case CustomAuthenticationSlide.CRYPT_TYPE_FINGER:
if (!_fingerAuthenticated) {
case CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC:
if (_bioSlot == null) {
return false;
}
// intentional fallthrough
case CustomAuthenticationSlide.CRYPT_TYPE_PASS:
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm);
if (EditTextHelper.getEditTextChars(_textPassword).length > 0) {
return EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) || _checkPasswordVisibility.isChecked();
}
return false;
default:
throw new RuntimeException();
return false;
}
}
@Override
public void onUserIllegallyRequestedNextPage() {
String message;
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm)) {
if (!EditTextHelper.areEditTextsEqual(_textPassword, _textPasswordConfirm) && !_checkPasswordVisibility.isChecked()) {
message = getString(R.string.password_equality_error);
} else if (!_fingerAuthenticated) {
message = getString(R.string.register_fingerprint);
} else {
return;
}
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
View view = getView();
if (view != null) {
Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_LONG);
snackbar.show();
}
} else if (_bioSlot == null) {
showBiometricPrompt();
}
}
@Override
public void onAuthenticated() {
_fingerAuthenticated = true;
}
private class BiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onError() {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
_bioSlot = slot;
_bioCipher = cipher;
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(CustomAuthenticatedSlide.this.getContext(), errString, Toast.LENGTH_LONG).show();
}
}
}
}
@@ -1,10 +1,7 @@
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,16 +9,18 @@ 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 androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.github.paolorotolo.appintro.ISlidePolicy;
import com.google.android.material.snackbar.Snackbar;
public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy, RadioGroup.OnCheckedChangeListener {
public static final int CRYPT_TYPE_INVALID = 0;
public static final int CRYPT_TYPE_NONE = 1;
public static final int CRYPT_TYPE_PASS = 2;
public static final int CRYPT_TYPE_FINGER = 3;
public static final int CRYPT_TYPE_BIOMETRIC = 3;
private RadioGroup _buttonGroup;
private int _bgColor;
@@ -34,10 +33,9 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
onCheckedChanged(_buttonGroup, _buttonGroup.getCheckedRadioButtonId());
// only enable the fingerprint option if the api version is new enough, permission is granted and a scanner is found
FingerprintManager manager = FingerprintHelper.getManager(getContext());
if (manager != null) {
RadioButton button = view.findViewById(R.id.rb_fingerprint);
TextView text = view.findViewById(R.id.text_rb_fingerprint);
if (BiometricsHelper.isAvailable(getContext())) {
RadioButton button = view.findViewById(R.id.rb_biometrics);
TextView text = view.findViewById(R.id.text_rb_biometrics);
button.setEnabled(true);
text.setEnabled(true);
}
@@ -75,8 +73,8 @@ public class CustomAuthenticationSlide extends Fragment implements ISlidePolicy,
case R.id.rb_password:
id = CRYPT_TYPE_PASS;
break;
case R.id.rb_fingerprint:
id = CRYPT_TYPE_FINGER;
case R.id.rb_biometrics:
id = CRYPT_TYPE_BIOMETRIC;
break;
default:
throw new RuntimeException();
@@ -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.vault.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,10 +4,12 @@ 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;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, Void, Result> {
import androidx.annotation.CallSuper;
public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Params, String, Result> {
private ProgressDialog _dialog;
public ProgressDialogTask(Context context, String message) {
@@ -31,6 +33,13 @@ public abstract class ProgressDialogTask<Params, Result> extends AsyncTask<Param
}
}
@Override
protected void onProgressUpdate(String... values) {
if (values.length == 1) {
_dialog.setMessage(values[0]);
}
}
protected void setPriority() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
}
@@ -2,21 +2,20 @@ 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.MasterKey;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.vault.slots.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
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 com.beemdevelopment.aegis.db.slots.SlotIntegrityException;
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, MasterKey> {
public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTask.Params, SlotListTask.Result> {
private Callback _cb;
private Class<T> _type;
@@ -27,51 +26,80 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
}
@Override
protected MasterKey doInBackground(SlotListTask.Params... args) {
protected Result doInBackground(SlotListTask.Params... args) {
setPriority();
Params params = args[0];
SlotList slots = params.getSlots();
try {
if (!slots.has(_type)) {
throw new RuntimeException();
}
MasterKey masterKey = null;
for (Slot slot : slots.findAll(_type)) {
try {
if (slot instanceof PasswordSlot) {
char[] password = (char[])params.getObj();
SecretKey key = ((PasswordSlot)slot).deriveKey(password);
Cipher cipher = slot.createDecryptCipher(key);
masterKey = slot.getKey(cipher);
} else if (slot instanceof FingerprintSlot) {
masterKey = slot.getKey((Cipher)params.getObj());
} else {
throw new RuntimeException();
}
break;
} catch (SlotIntegrityException e) {
for (Slot slot : slots.findAll(_type)) {
try {
if (slot instanceof PasswordSlot) {
char[] password = (char[]) params.getObj();
return decryptPasswordSlot((PasswordSlot) slot, password);
} else if (slot instanceof BiometricSlot) {
return decryptBiometricSlot((BiometricSlot) slot, (Cipher) params.getObj());
}
}
} catch (SlotException e) {
throw new RuntimeException(e);
} catch (SlotIntegrityException ignored) {
if (masterKey == null) {
throw new SlotIntegrityException();
}
return masterKey;
} catch (SlotIntegrityException e) {
return null;
} catch (SlotException e) {
throw new RuntimeException(e);
}
return null;
}
private Result decryptBiometricSlot(BiometricSlot slot, Cipher cipher)
throws SlotException, SlotIntegrityException {
MasterKey key = slot.getKey(cipher);
return new Result(key, slot);
}
private Result decryptPasswordSlot(PasswordSlot slot, char[] password)
throws SlotIntegrityException, SlotException {
MasterKey masterKey;
SecretKey key = slot.deriveKey(password);
byte[] oldPasswordBytes = CryptoUtils.toBytesOld(password);
try {
masterKey = decryptPasswordSlot(slot, key);
} catch (SlotIntegrityException e) {
// a bug introduced in afb9e59 caused passwords longer than 64 bytes to produce a different key than before
// so, try again with the old password encode function if the password is longer than 64 bytes
if (slot.isRepaired() || oldPasswordBytes.length <= 64) {
throw e;
}
publishProgress(getDialog().getContext().getString(R.string.unlocking_vault_repair));
// try to decrypt the password slot with the old key
SecretKey oldKey = slot.deriveKey(oldPasswordBytes);
masterKey = decryptPasswordSlot(slot, oldKey);
}
// if necessary, repair the slot by re-encrypting the master key with the correct key
// slots with passwords smaller than 64 bytes also get this treatment to make sure those also have 'repaired' set to true
boolean repaired = false;
if (!slot.isRepaired()) {
Cipher cipher = Slot.createEncryptCipher(key);
slot.setKey(masterKey, cipher);
repaired = true;
}
return new Result(masterKey, slot, repaired);
}
private MasterKey decryptPasswordSlot(PasswordSlot slot, SecretKey key)
throws SlotException, SlotIntegrityException {
Cipher cipher = slot.createDecryptCipher(key);
return slot.getKey(cipher);
}
@Override
protected void onPostExecute(MasterKey masterKey) {
super.onPostExecute(masterKey);
_cb.onTaskFinished(masterKey);
protected void onPostExecute(Result result) {
super.onPostExecute(result);
_cb.onTaskFinished(result);
}
public static class Params {
@@ -92,7 +120,35 @@ public class SlotListTask<T extends Slot> extends ProgressDialogTask<SlotListTas
}
}
public static class Result {
private MasterKey _key;
private Slot _slot;
private boolean _repaired;
public Result(MasterKey key, Slot slot, boolean repaired) {
_key = key;
_slot = slot;
_repaired = repaired;
}
public Result(MasterKey key, Slot slot) {
this(key, slot, false);
}
public MasterKey getKey() {
return _key;
}
public Slot getSlot() {
return _slot;
}
public boolean isSlotRepaired() {
return _repaired;
}
}
public interface Callback {
void onTaskFinished(MasterKey key);
void onTaskFinished(Result result);
}
}
@@ -1,10 +1,16 @@
package com.beemdevelopment.aegis.ui.views;
import androidx.recyclerview.widget.RecyclerView;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.ItemTouchHelperAdapter;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
@@ -13,59 +19,111 @@ 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;
public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements ItemTouchHelperAdapter {
private List<DatabaseEntry> _entries;
private List<DatabaseEntry> _shownEntries;
private static Listener _listener;
private EntryListView _view;
private List<VaultEntry> _entries;
private List<VaultEntry> _shownEntries;
private VaultEntry _selectedEntry;
private VaultEntry _focusedEntry;
private boolean _showAccountName;
private boolean _searchAccountName;
private boolean _highlightEntry;
private boolean _tapToReveal;
private int _tapToRevealTime;
private String _groupFilter;
private SortCategory _sortCategory;
private ViewMode _viewMode;
private String _searchFilter;
private boolean _isPeriodUniform = true;
private Handler _dimHandler;
// keeps track of the viewholders that are currently bound
private List<EntryHolder> _holders;
public EntryAdapter(Listener listener) {
public EntryAdapter(EntryListView view) {
_entries = new ArrayList<>();
_shownEntries = new ArrayList<>();
_holders = new ArrayList<>();
_listener = listener;
_dimHandler = new Handler();
_view = view;
}
public void destroy() {
for (EntryHolder holder : _holders) {
holder.destroy();
}
_view = null;
}
public void setShowAccountName(boolean showAccountName) {
_showAccountName = showAccountName;
}
public void addEntry(DatabaseEntry entry) {
_entries.add(entry);
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
public void setTapToReveal(boolean tapToReveal) {
_tapToReveal = tapToReveal;
}
public void addEntries(List<DatabaseEntry> entries) {
_entries.addAll(entries);
for (DatabaseEntry entry : entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
public void setTapToRevealTime(int number) {
_tapToRevealTime = number;
}
public void setSearchAccountName(boolean searchAccountName) {
_searchAccountName = searchAccountName;
}
public void setHighlightEntry(boolean highlightEntry) {
_highlightEntry = highlightEntry;
}
public VaultEntry getEntryAt(int position) {
return _shownEntries.get(position);
}
public void addEntry(VaultEntry entry) {
_entries.add(entry);
if (isEntryFiltered(entry)) {
return;
}
boolean added = false;
Comparator<VaultEntry> 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;
}
}
}
notifyDataSetChanged();
if (!added){
_shownEntries.add(entry);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
checkPeriodUniformity();
}
public void removeEntry(DatabaseEntry entry) {
entry = getEntryByUUID(entry.getUUID());
public void addEntries(List<VaultEntry> entries) {
_entries.addAll(entries);
updateShownEntries();
checkPeriodUniformity(true);
}
public void removeEntry(VaultEntry entry) {
_entries.remove(entry);
if (_shownEntries.contains(entry)) {
@@ -73,16 +131,18 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
_shownEntries.remove(position);
notifyItemRemoved(position);
}
checkPeriodUniformity();
}
public void clearEntries() {
_entries.clear();
_shownEntries.clear();
notifyDataSetChanged();
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry newEntry) {
DatabaseEntry oldEntry = getEntryByUUID(newEntry.getUUID());
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
_entries.set(_entries.indexOf(oldEntry), newEntry);
if (_shownEntries.contains(oldEntry)) {
@@ -101,23 +161,30 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
int position = getItemCount() - 1;
notifyItemInserted(position);
}
checkPeriodUniformity();
}
private boolean isEntryFiltered(DatabaseEntry entry) {
private boolean isEntryFiltered(VaultEntry entry) {
String group = entry.getGroup();
if (_groupFilter == null) {
return false;
}
return group == null || !group.equals(_groupFilter);
}
String issuer = entry.getIssuer().toLowerCase();
String name = entry.getName().toLowerCase();
private DatabaseEntry getEntryByUUID(UUID uuid) {
for (DatabaseEntry entry : _entries) {
if (entry.getUUID().equals(uuid)) {
return entry;
if (_groupFilter != null) {
if (group == null && _groupFilter.equals(_view.getContext().getString(R.string.filter_ungrouped))) {
return false;
}
if (group == null || !group.equals(_groupFilter)) {
return true;
}
}
throw new AssertionError("no entry found with the same id");
if (_searchFilter == null) {
return false;
}
return !issuer.contains(_searchFilter) && !(_searchAccountName && name.contains(_searchFilter));
}
public void refresh(boolean hard) {
@@ -130,17 +197,57 @@ 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();
}
}
public void setSearchFilter(String search) {
_searchFilter = search != null ? search.toLowerCase() : null;
updateShownEntries();
}
private void updateShownEntries() {
// clear the list of shown entries first
_shownEntries.clear();
for (DatabaseEntry entry : _entries) {
// add entries back that are not filtered out
for (VaultEntry entry : _entries) {
if (!isEntryFiltered(entry)) {
_shownEntries.add(entry);
}
}
// sort the remaining list of entries
Comparator<VaultEntry> comparator = _sortCategory.getComparator();
if (comparator != null) {
Collections.sort(_shownEntries, comparator);
}
notifyDataSetChanged();
}
public void setViewMode(ViewMode viewMode) {
_viewMode = viewMode;
}
@Override
public void onItemDismiss(int position) {
@@ -153,7 +260,7 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return;
}
_listener.onEntryDrop(_shownEntries.get(position));
_view.onEntryDrop(_shownEntries.get(position));
}
@Override
@@ -163,8 +270,8 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return;
}
// notify the database first
_listener.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// notify the vault first
_view.onEntryMove(_entries.get(firstPosition), _entries.get(secondPosition));
// update our side of things
Collections.swap(_entries, firstPosition, secondPosition);
@@ -172,10 +279,18 @@ 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);
return new EntryHolder(view);
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View view = inflater.inflate(_viewMode.getLayoutId(), parent, false);
EntryHolder holder = new EntryHolder(view);
_view.setPreloadView(holder.getIconView());
return holder;
}
@Override
@@ -186,25 +301,46 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
@Override
public void onBindViewHolder(final EntryHolder holder, int position) {
DatabaseEntry entry = _shownEntries.get(position);
VaultEntry entry = _shownEntries.get(position);
holder.setFocused(entry == _selectedEntry);
boolean hidden = _tapToReveal && entry != _focusedEntry;
boolean dimmed = _highlightEntry && _focusedEntry != null && _focusedEntry != entry;
boolean showProgress = !isPeriodUniform() && entry.getInfo() instanceof TotpInfo;
holder.setData(entry, _showAccountName, showProgress);
if (showProgress) {
holder.startRefreshLoop();
}
holder.setData(entry, _showAccountName, showProgress, hidden, dimmed);
holder.loadIcon(_view);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = holder.getAdapterPosition();
_listener.onEntryClick(_shownEntries.get(position));
boolean handled = false;
if (_selectedEntry == null) {
if (_highlightEntry || _tapToReveal) {
if (_focusedEntry == entry) {
resetFocus();
handled = true;
} else {
focusEntry(entry);
}
}
}
if (!handled) {
_view.onEntryClick(entry);
}
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
int position = holder.getAdapterPosition();
return _listener.onLongEntryClick(_shownEntries.get(position));
if (_selectedEntry == null) {
setSelectedEntry(_shownEntries.get(position));
holder.setFocused(true);
}
return _view.onLongEntryClick(_shownEntries.get(position));
}
});
holder.setOnRefreshClickListener(new View.OnClickListener() {
@@ -212,14 +348,14 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
public void onClick(View v) {
// this will only be called if the entry is of type HotpInfo
try {
((HotpInfo)entry.getInfo()).incrementCounter();
((HotpInfo) entry.getInfo()).incrementCounter();
} catch (OtpInfoException e) {
throw new RuntimeException(e);
}
// notify the listener that the counter has been incremented
// this gives it a chance to save the database
_listener.onEntryChange(entry);
// this gives it a chance to save the vault
_view.onEntryChange(entry);
// finally, refresh the code in the UI
holder.refreshCode();
@@ -229,9 +365,27 @@ 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);
}
_view.onPeriodUniformityChanged(_isPeriodUniform);
}
public int getUniformPeriod() {
List<TotpInfo> infos = new ArrayList<>();
for (DatabaseEntry entry : _shownEntries) {
for (VaultEntry entry : _shownEntries) {
OtpInfo info = entry.getInfo();
if (info instanceof TotpInfo) {
infos.add((TotpInfo) info);
@@ -252,6 +406,58 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
return period;
}
private void focusEntry(VaultEntry entry) {
_focusedEntry = entry;
_dimHandler.removeCallbacksAndMessages(null);
for (EntryHolder holder : _holders) {
if (holder.getEntry() != _focusedEntry) {
if (_highlightEntry) {
holder.dim();
}
if (_tapToReveal) {
holder.hideCode();
}
} else {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.revealCode();
}
}
}
_dimHandler.postDelayed(this::resetFocus, _tapToRevealTime * 1000);
}
private void resetFocus() {
for (EntryHolder holder : _holders) {
if (_highlightEntry) {
holder.highlight();
}
if (_tapToReveal) {
holder.hideCode();
}
}
_focusedEntry = null;
}
public void setSelectedEntry(VaultEntry entry) {
if (entry == null) {
notifyItemChanged(_shownEntries.indexOf(_selectedEntry));
} else if (_highlightEntry) {
resetFocus();
}
_selectedEntry = entry;
}
public boolean isDragAndDropAllowed() {
return _sortCategory == SortCategory.CUSTOM && _groupFilter == null && _searchFilter == null;
}
public boolean isPeriodUniform() {
return getUniformPeriod() != -1;
}
@@ -262,10 +468,11 @@ public class EntryAdapter extends RecyclerView.Adapter<EntryHolder> implements I
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
boolean onLongEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onEntryClick(VaultEntry entry);
boolean onLongEntryClick(VaultEntry entry);
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onPeriodUniformityChanged(boolean uniform);
}
}
@@ -1,37 +1,49 @@
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.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.helpers.ThemeHelper;
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 com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class EntryHolder extends RecyclerView.ViewHolder {
private static final float DEFAULT_ALPHA = 1.0f;
private static final float DIMMED_ALPHA = 0.2f;
private TextView _profileName;
private TextView _profileCode;
private TextView _profileIssuer;
private ImageView _profileDrawable;
private DatabaseEntry _entry;
private VaultEntry _entry;
private ImageView _buttonRefresh;
private boolean _hidden;
private PeriodProgressBar _progressBar;
private View _view;
private UiRefresher _refresher;
public EntryHolder(final View view) {
super(view);
_view = view.findViewById(R.id.rlCardEntry);
_profileName = view.findViewById(R.id.profile_account_name);
_profileCode = view.findViewById(R.id.profile_code);
_profileIssuer = view.findViewById(R.id.profile_issuer);
@@ -41,11 +53,15 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_progressBar = view.findViewById(R.id.progressBar);
int primaryColorId = view.getContext().getResources().getColor(R.color.colorPrimary);
_progressBar.getProgressDrawable().setColorFilter(primaryColorId, PorterDuff.Mode.SRC_IN);
_view.setBackground(_view.getContext().getResources().getDrawable(R.color.card_background));
_refresher = new UiRefresher(new UiRefresher.Listener() {
@Override
public void onRefresh() {
refreshCode();
if (_hidden) {
refreshCode();
}
_progressBar.refresh();
}
@@ -56,14 +72,12 @@ public class EntryHolder extends RecyclerView.ViewHolder {
});
}
public void setData(DatabaseEntry entry, boolean showAccountName, boolean showProgress) {
public void setData(VaultEntry entry, boolean showAccountName, boolean showProgress, boolean hidden, boolean dimmed) {
_entry = entry;
_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);
@@ -74,22 +88,68 @@ public class EntryHolder extends RecyclerView.ViewHolder {
_profileName.setText(" - " + entry.getName());
}
if (_entry.hasIcon()) {
byte[] imageBytes = entry.getIcon();
Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
_profileDrawable.setImageBitmap(image);
if (_hidden) {
hideCode();
} else {
TextDrawable drawable = TextDrawableHelper.generate(entry.getIssuer(), entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
refreshCode();
}
refreshCode();
itemView.setAlpha(dimmed ? DIMMED_ALPHA : DEFAULT_ALPHA);
}
public VaultEntry getEntry() {
return _entry;
}
public void loadIcon(Fragment fragment) {
if (_entry.hasIcon()) {
Glide.with(fragment)
.asDrawable()
.load(_entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_profileDrawable);
} else {
TextDrawable drawable = TextDrawableHelper.generate(_entry.getIssuer(), _entry.getName(), _profileDrawable);
_profileDrawable.setImageDrawable(drawable);
}
}
public ImageView getIconView() {
return _profileDrawable;
}
public void setOnRefreshClickListener(View.OnClickListener listener) {
_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());
startRefreshLoop();
} else {
stopRefreshLoop();
}
}
public void setFocused(boolean focused) {
if (focused) {
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, _view.getContext().getTheme()));
} else {
_view.setBackgroundColor(ThemeHelper.getThemeColor(R.attr.cardBackground, _view.getContext().getTheme()));
}
_view.setSelected(focused);
}
public void destroy() {
_refresher.destroy();
}
public void startRefreshLoop() {
_refresher.start();
}
@@ -99,7 +159,48 @@ public class EntryHolder extends RecyclerView.ViewHolder {
}
public void refreshCode() {
String otp = _entry.getInfo().getOtp();
_profileCode.setText(otp.substring(0, otp.length() / 2) + " " + otp.substring(otp.length() / 2));
if (!_hidden) {
updateCode();
}
}
private void updateCode() {
OtpInfo info = _entry.getInfo();
String otp = info.getOtp();
if (!(info instanceof SteamInfo)) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < otp.length(); i++) {
if (i != 0 && i % 3 == 0) {
sb.append(" ");
}
sb.append(otp.charAt(i));
}
otp = sb.toString();
}
_profileCode.setText(otp);
}
public void revealCode() {
updateCode();
_hidden = false;
}
public void hideCode() {
_profileCode.setText(R.string.tap_to_reveal);
_hidden = true;
}
public void dim() {
animateAlphaTo(DIMMED_ALPHA);
}
public void highlight() {
animateAlphaTo(DEFAULT_ALPHA);
}
private void animateAlphaTo(float alpha) {
itemView.animate().alpha(alpha).setDuration(200).start();
}
}
@@ -1,35 +1,53 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
import com.beemdevelopment.aegis.helpers.UiRefresher;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.bumptech.glide.Glide;
import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.util.ViewPreloadSizeProvider;
import java.util.Collections;
import java.util.List;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class EntryListView extends Fragment implements EntryAdapter.Listener {
private EntryAdapter _adapter;
private Listener _listener;
private SimpleItemTouchHelperCallback _touchCallback;
private RecyclerView _rvKeyProfiles;
private RecyclerView _recyclerView;
private RecyclerView.ItemDecoration _dividerDecoration;
private ViewPreloadSizeProvider<VaultEntry> _preloadSizeProvider;
private PeriodProgressBar _progressBar;
private boolean _showProgress;
private ViewMode _viewMode;
private LinearLayout _emptyStateView;
private UiRefresher _refresher;
@@ -40,26 +58,43 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
_showProgress = false;
}
@Override
public void onDestroy() {
_adapter.destroy();
super.onDestroy();
}
@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);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(view.getContext());
_rvKeyProfiles.setLayoutManager(mLayoutManager);
_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);
_listener.onScroll(dx, dy);
}
});
// set up icon preloading
_preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<VaultEntry> preloader = new RecyclerViewPreloader<>(Glide.with(this), modelProvider, _preloadSizeProvider, 10);
_recyclerView.addOnScrollListener(preloader);
LinearLayoutManager layoutManager = new LinearLayoutManager(view.getContext());
_recyclerView.setLayoutManager(layoutManager);
_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
@@ -73,15 +108,54 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
}
});
_emptyStateView = view.findViewById(R.id.vEmptyList);
return view;
}
public void setGroupFilter(String group) {
_adapter.setGroupFilter(group);
_touchCallback.setIsLongPressDragEnabled(group == null);
checkPeriodUniformity();
public void setPreloadView(View view) {
_preloadSizeProvider.setView(view);
}
runLayoutAnimation(_rvKeyProfiles);
@Override
public void onDestroyView() {
_refresher.destroy();
super.onDestroyView();
}
public void setGroupFilter(String group, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(group == null);
_adapter.setGroupFilter(group, apply);
if (apply) {
runEntriesAnimation();
}
}
public void setActionModeState(boolean enabled, VaultEntry entry) {
_touchCallback.setSelectedEntry(entry);
_touchCallback.setIsLongPressDragEnabled(enabled && _adapter.isDragAndDropAllowed());
_adapter.setSelectedEntry(entry);
}
public void setSortCategory(SortCategory sortCategory, boolean apply) {
_touchCallback.setIsLongPressDragEnabled(sortCategory == SortCategory.CUSTOM);
_adapter.setSortCategory(sortCategory, apply);
if (apply) {
runEntriesAnimation();
}
}
public void setSearchFilter(String search) {
_touchCallback.setIsLongPressDragEnabled(search == null);
_adapter.setSearchFilter(search);
}
public void setViewMode(ViewMode mode) {
_viewMode = mode;
updateDividerDecoration();
_adapter.setViewMode(_viewMode);
}
public void refresh(boolean hard) {
@@ -91,105 +165,178 @@ 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;
}
@Override
public void onEntryClick(DatabaseEntry entry) {
public void onEntryClick(VaultEntry entry) {
_listener.onEntryClick(entry);
}
@Override
public boolean onLongEntryClick(DatabaseEntry entry) {
return false;
public boolean onLongEntryClick(VaultEntry entry) {
_listener.onLongEntryClick(entry);
return true;
}
@Override
public void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2) {
public void onEntryMove(VaultEntry entry1, VaultEntry entry2) {
_listener.onEntryMove(entry1, entry2);
}
@Override
public void onEntryDrop(DatabaseEntry entry) {
public void onEntryDrop(VaultEntry entry) {
_listener.onEntryDrop(entry);
}
@Override
public void onEntryChange(DatabaseEntry entry) {
public void onEntryChange(VaultEntry entry) {
_listener.onEntryChange(entry);
}
@Override
public void onPeriodUniformityChanged(boolean isUniform) {
setShowProgress(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);
}
public void addEntry(DatabaseEntry entry) {
public void setSearchAccountName(boolean searchAccountName) {
_adapter.setSearchAccountName(searchAccountName);
}
public void setHighlightEntry(boolean highlightEntry) {
_adapter.setHighlightEntry(highlightEntry);
}
public void setTapToReveal(boolean tapToReveal) {
_adapter.setTapToReveal(tapToReveal);
}
public void setTapToRevealTime(int number) {
_adapter.setTapToRevealTime(number);
}
public void addEntry(VaultEntry entry) {
_adapter.addEntry(entry);
checkPeriodUniformity();
updateEmptyState();
}
public void addEntries(List<DatabaseEntry> entries) {
public void addEntries(List<VaultEntry> entries) {
_adapter.addEntries(entries);
checkPeriodUniformity();
updateEmptyState();
}
public void removeEntry(DatabaseEntry entry) {
public void removeEntry(VaultEntry entry) {
_adapter.removeEntry(entry);
checkPeriodUniformity();
updateEmptyState();
}
public void clearEntries() {
_adapter.clearEntries();
checkPeriodUniformity();
}
public void replaceEntry(DatabaseEntry entry) {
_adapter.replaceEntry(entry);
checkPeriodUniformity();
public void replaceEntry(VaultEntry oldEntry, VaultEntry newEntry) {
_adapter.replaceEntry(oldEntry, newEntry);
}
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();
}
private void setShowProgress(boolean showProgress) {
_showProgress = showProgress;
updateDividerDecoration();
}
private void updateDividerDecoration() {
if (_dividerDecoration != null) {
_recyclerView.removeItemDecoration(_dividerDecoration);
}
float height = _viewMode.getDividerHeight();
if (_showProgress && height == 0) {
DividerItemDecoration divider = new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL);
divider.setDrawable(ContextCompat.getDrawable(getContext(), R.drawable.entry_divider));
_dividerDecoration = divider;
} else {
_dividerDecoration = new VerticalSpaceItemDecoration(height);
}
_recyclerView.addItemDecoration(_dividerDecoration);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_recyclerView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_recyclerView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
public interface Listener {
void onEntryClick(DatabaseEntry entry);
void onEntryMove(DatabaseEntry entry1, DatabaseEntry entry2);
void onEntryDrop(DatabaseEntry entry);
void onEntryChange(DatabaseEntry entry);
void onEntryClick(VaultEntry entry);
void onEntryMove(VaultEntry entry1, VaultEntry entry2);
void onEntryDrop(VaultEntry entry);
void onEntryChange(VaultEntry entry);
void onLongEntryClick(VaultEntry entry);
void onScroll(int dx, int dy);
}
private class VerticalSpaceItemDecoration extends RecyclerView.ItemDecoration {
private int _height;
private VerticalSpaceItemDecoration(float dp) {
// convert dp to pixels
_height = (int) (dp * (getContext().getResources().getDisplayMetrics().densityDpi / 160f));
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
if (parent.getChildAdapterPosition(view) == 0) {
// the first item should also have a top margin
outRect.top = _height;
}
outRect.bottom = _height;
}
}
private class IconPreloadProvider implements ListPreloader.PreloadModelProvider<VaultEntry> {
@NonNull
@Override
public List<VaultEntry> getPreloadItems(int position) {
VaultEntry entry = _adapter.getEntryAt(position);
if (!entry.hasIcon()) {
return Collections.emptyList();
}
return Collections.singletonList(entry);
}
@Nullable
@Override
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
return Glide.with(EntryListView.this)
.asDrawable()
.load(entry)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
}
@@ -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,86 @@
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.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<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);
}
}
}
}

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