Compare commits

..
314 Commits
Author SHA1 Message Date
Alexander Bakker 7506c70236 Release v2.0
build / build (push) Canceled after 0s
2021-05-26 10:50:24 +02:00
Alexander Bakker d1e3224cba Store the group filter as JSON in preferences
This ensures we don't run into issues if the group name contains a ','
2021-05-26 10:46:49 +02:00
Alexander Bakker d2eed8a356 Update translations from Crowdin 2021-05-26 10:37:07 +02:00
Alexander Bakker 668823507d Put the feature list in full_description.txt on a single line
This is an attempt to work around the broken rendering on the F-Droid website
2021-05-26 10:23:40 +02:00
Alexander Bakker 2b83dde58a Update in-app URL to aegis-icons latest release page 2021-05-26 10:18:03 +02:00
Alexander Bakker e97a53bbe9 Force search filter to null if it's empty
Fixes #705
2021-05-22 13:09:52 +02:00
Alexander Bakker 77f59d41ad Add group filter back in the UI tests 2021-05-22 12:45:58 +02:00
Alexander Bakker e2ddc8a80f Try to guess the MIME type if SAF doesn't provide it
This fixes a crash that would occur if SAF doesn't provide a MIME type
2021-05-20 21:46:56 +02:00
Alexander Bakker b6093c2bfa Explicitly disable backups preferences when disabling encryption 2021-05-20 21:25:56 +02:00
Alexander Bakker fc25312d12 Add an option to save the current group filter 2021-05-20 20:58:20 +02:00
Alexander Bakker 51eade900c Fix a number of issues related to updating the group chip after changes 2021-05-19 12:56:35 +02:00
Alexander Bakker d152d13580 Make sure text and radio button are aligned in importer dialog 2021-05-09 11:58:24 +02:00
Alexander Bakker 7dff3fbad1 Release v2.0-beta2
build / build (push) Canceled after 0s
2021-05-08 11:04:59 +02:00
Alexander Bakker 7dc7f6ccc0 Update translations from Crowdin 2021-05-08 10:57:05 +02:00
Alexander Bakker ad0088eca6 Don't rely on the intent action to decide whether to block auto lock 2021-05-08 10:42:04 +02:00
Alexander Bakker 813bf292fa Update dependencies 2021-05-08 10:10:08 +02:00
Alexander Bakker 80bf5bc854 Fix the QR code background color for the light theme 2021-05-07 18:13:46 +02:00
Alexander Bakker e319a2eceb Fix a crash when opening the "Import from app" dialog 2021-05-07 18:13:17 +02:00
Alexander Bakker 0a9bfde438 Hide the keyboard before every slide change in the intro 2021-05-07 18:12:52 +02:00
Alexander Bakker c3c4e1fb4b Reflect changes in the group list to the group filter
Fixes #723
2021-05-07 18:08:39 +02:00
Alexander Bakker 1eff91594f Fix multiple layout issues on small-screen devices
Close #560
2021-05-06 23:17:35 +02:00
Alexander Bakker fddc29880a Add support from importing from 2FAS Authenticator 2021-05-05 10:58:22 +02:00
Alexander BakkerandGitHub 2ccfcd62e1 Merge pull request #734 from EwoutH/gradle
Gradle: Validate wrapper in CI and update to 6.8.3
2021-05-04 22:40:29 +02:00
Ewout ter Hoeven 993b29bd83 Gradle: Update wrapper and scripts to 6.8.3 2021-05-04 21:59:25 +02:00
Ewout ter Hoeven e371485a1b CI: Add step that validates the Gradle wrapper
Validates the gradle-wrapper.jar file on each build, which is a binary blob of executable code. This action ensures it's legit and doesn't execute malicious code.

See https://github.com/gradle/wrapper-validation-action
2021-05-04 21:59:25 +02:00
Alexander Bakker fb58c877d1 Remove padding from Y plane data before passing it to ZXing
Fixes #726
Fixes #657
2021-05-04 19:21:57 +02:00
Alexander BakkerandGitHub a62e474e33 Merge pull request #735 from EwoutH/imgbot
[ImgBot] Optimize images
2021-04-29 21:03:38 +02:00
ImgBotApp 5975b929b7 [ImgBot] Optimize images
*Total -- 569.55kb -> 504.66kb (11.39%)

/app/src/main/res/drawable-xhdpi/ic_qr_code.png -- 3.27kb -> 2.13kb (34.81%)
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -- 6.07kb -> 3.97kb (34.54%)
/app/src/main/res/drawable-xxhdpi/ic_qr_code.png -- 6.25kb -> 4.32kb (30.86%)
/metadata/en-US/images/icon.png -- 33.36kb -> 23.19kb (30.47%)
/app/src/main/res/drawable-hdpi/ic_qr_code.png -- 2.62kb -> 1.86kb (29.11%)
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -- 4.00kb -> 2.95kb (26.2%)
/app/src/main/res/drawable-mdpi/ic_qr_code.png -- 1.55kb -> 1.17kb (24.7%)
/app/src/main/res/drawable/app_icon.png -- 143.85kb -> 113.38kb (21.18%)
/app/src/main/res/drawable/ic_aegis_iconx.png -- 19.38kb -> 15.30kb (21.04%)
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -- 2.21kb -> 1.96kb (11.42%)
/metadata/en-US/images/phoneScreenshots/screenshot4.png -- 186.71kb -> 177.50kb (4.93%)
/docs/diagram.svg -- 11.00kb -> 10.58kb (3.85%)
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -- 1.58kb -> 1.53kb (3.22%)
/metadata/en-US/images/phoneScreenshots/screenshot5.png -- 147.70kb -> 144.82kb (1.95%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2021-04-28 09:18:59 +00:00
Alexander Bakker a5d3de55a2 Add support for selecting SVG icons through the file selection dialog
Fixes #724
2021-04-14 22:52:00 +02:00
Alexander Bakker b195d0dadd Fix multiple icon rendering issues in card_icon
- Use software rendering on API <= 27
- Use ImageView instead of CircleImageView

Fixes #725
2021-04-14 21:43:59 +02:00
Alexander Bakker 7d3666a5d0 Make the password configuration dialog scrollable
Fixes #722
2021-04-14 17:33:16 +02:00
Alexander Bakker 61ac83b134 Release v2.0-beta1
build / build (push) Canceled after 0s
2021-04-14 13:40:04 +02:00
Alexander Bakker 9c907cf14a Apply top margin to the first entry if there are no groups 2021-04-14 13:33:40 +02:00
Alexander Bakker 5f737582fb Fix syntax issue in arrays.xml 2021-04-14 12:56:31 +02:00
Alexander Bakker 89256dfef5 Update translations from Crowdin 2021-04-14 12:54:21 +02:00
Alexander Bakker 94b4c098de Update icons packs documentation 2021-04-14 12:15:19 +02:00
Alexander Bakker 1d848947d9 Only set the query if its non-empty when switching icon packs
This fixes an issue where the categories would be hidden when switching icon packs
2021-04-13 17:21:10 +02:00
Alexander Bakker 43ebe402ce Fix crash caused by incorrect cast to PreferencesFragment 2021-04-11 13:54:05 +02:00
Alexander BakkerandGitHub ae0b00573f Merge pull request #539 from alexbakker/iconpacks
Introduce support for icon packs
2021-04-10 10:50:36 +02:00
Alexander BakkerandGitHub efb5b10a76 Merge pull request #716 from DerEnderKeks/fix-recent-apps-behavior
Fixed that app vanished from recent apps list after locking
2021-04-09 14:01:58 +02:00
Alexander Bakker 4f38988c0d Introduce support for icon packs 2021-04-07 17:28:08 +02:00
Alexander Bakker c977b9a064 Update dependencies 2021-04-03 11:24:13 +02:00
DerEnderKeks 5353e7c3b1 Changed method to finish activities when locking 2021-04-01 18:03:16 +02:00
DerEnderKeks b999ccc83b Fixed that app vanished from recent apps list after locking 2021-03-31 17:06:39 +02:00
Alexander BakkerandGitHub fea5b8e75a Merge pull request #715 from DerEnderKeks/patch-1
Fixed minor markup irregularities in bug report template
2021-03-31 12:58:47 +02:00
DerEnderKeksandGitHub 6ce661180e Fixed minor markup irregularities in bug report template 2021-03-31 12:50:43 +02:00
Alexander Bakker 230f21b9f0 Update dependencies 2021-03-20 13:11:17 +01:00
Alexander Bakker 801e677928 Silence an overly enthousiastic linker check 2021-03-01 18:44:14 +01:00
Alexander Bakker 143b9a85b4 Update dependencies 2021-03-01 18:21:20 +01:00
Alexander Bakker 71f66e3142 Remove old testdata and update documentation to new location 2021-02-21 17:16:40 +01:00
Alexander Bakker a6ab669811 Don't crash when uiMode is set to UI_MODE_NIGHT_UNDEFINED 2021-02-21 17:08:05 +01:00
Alexander Bakker a519b31bd3 Fix typo in CONTRIBUTING.md 2021-02-18 13:10:26 +01:00
Alexander Bakker c7ef10367f Fix an issue where advanced fields were disabled inadvertently 2021-02-17 12:59:59 +01:00
Alexander BakkerandGitHub 085e8e51dc Merge pull request #688 from jhenninger/feature/receive-shared-image
Add support for creating entries from shared images
2021-02-17 12:01:17 +01:00
Johannes Henninger 167f2d6dad Add support for creating entries from shared images 2021-02-14 16:05:13 +01:00
Alexander Bakker e653e42455 Update dependencies 2021-02-14 16:03:30 +01:00
Alexander Bakker 158eb4609c Remove unused 'stateless' support code from AuthActivity 2021-02-13 19:15:30 +01:00
Alexander Bakker 2242dbc6cd Update build badge for GitHub Actions 2021-02-10 12:56:49 +01:00
Alexander Bakker ca8f3e49ea Hide the vault wipe option in the menu of the import activity
Always showing the dialog can be confusing for users.
2021-02-08 19:11:57 +01:00
Alexander Bakker c3d9ab86c7 Improve the wording of a couple of strings 2021-02-08 18:55:23 +01:00
Alexander Bakker 441a943a9d Update dependencies 2021-02-08 18:46:56 +01:00
Alexander Bakker 293e2ddeb0 When importing, read the file from SAF on a background thread
Fixes issue with importing from Nextcloud. Close #673.
2021-02-07 13:12:41 +01:00
Alexander Bakker 2c8a64f943 Disable some fields if Steam OTP type is selected
Also, move some magic default OTP parameters to constants
2021-01-30 16:41:25 +01:00
Alexander Bakker bc6cb35dc0 Clean up unused variables 2021-01-27 20:44:47 +01:00
Alexander Bakker ad138e4a4c Fix behavior of the new entry highlight in case no scroll is needed 2021-01-27 20:42:39 +01:00
Alexander Bakker f4bdf4645b Hide some entry editing settings by default to reduce user confusion 2021-01-27 20:09:53 +01:00
Alexander Bakker fd9a387b4e Rename Filter -> Sort in menu and remove old references to Filter 2021-01-27 18:32:44 +01:00
Alexander Bakker 778000aa24 Minor fixes for group chips, remove remnants of filter menu item 2021-01-27 18:24:52 +01:00
Alexander Bakker 40fbe39474 Update list of third-party licenses 2021-01-27 14:00:27 +01:00
Alexander Bakker bda1a1d5af Add a dialog for Aegis' license 2021-01-27 13:54:11 +01:00
Alexander Bakker e6e2809c9c Fix the appearance of the toolbar in the scanner activity 2021-01-27 12:36:48 +01:00
Alexander Bakker b91355da47 Remove usage of HtmlCompat 2021-01-24 21:04:25 +01:00
Alexander Bakker 4bb5ee52d9 Fix disabled alert dialog button color by setting alpha using selector 2021-01-24 20:52:24 +01:00
Alexander Bakker de3fcfdb26 Fix the background color of the imported entries list 2021-01-24 20:44:01 +01:00
Alexander Bakker ae71febf10 Move import logic to separate activity to fix a couple of issues
Fixes #456.
Fixes #670.
2021-01-24 20:19:29 +01:00
Alexander Bakker 7be1a74cfd Force higher resolution for analysis and run it on a background thread 2021-01-24 15:02:02 +01:00
Alexander Bakker 22c93bf5c6 Show some additional warning info in the entry deletion dialog
Close #565.
2021-01-24 14:20:29 +01:00
Alexander Bakker 68436fba9c Rewrite layout of EditEntryActivity to not use TableLayout
This prevents excessive overdraw and fixes the UI tests
2021-01-24 12:53:29 +01:00
Alexander Bakker 44ff321f8f Remove pref_tools_group_title from all locales 2021-01-23 19:36:57 +01:00
Alexander Bakker d9d6733878 Improve preference organization and add summaries to the sections 2021-01-23 19:29:41 +01:00
Alexander Bakker eae31ef94b Switch from Appcompat to Material Components theme
Close #354.
2021-01-23 15:23:02 +01:00
Alexander BakkerandGitHub 29ea2df696 Merge pull request #671 from michaelschattgen/feature/chips
Start working on bottomsheet for group chips
2021-01-23 12:08:01 +01:00
Alexander BakkerandMichael Schättgen a6bf3b7c87 Add bottomsheet for chips 2021-01-21 10:53:37 +01:00
Alexander Bakker c6cb390990 Fix the UI tests 2021-01-16 17:43:07 +01:00
Alexander BakkerandGitHub f7a428b8cb Merge pull request #654 from alexbakker/android-backups
Add support for participating in Android's backup system
2021-01-16 16:24:08 +01:00
Alexander Bakker f080eaa8f9 Add support for participating in Android's backup system
This adds an option for participating in Android's backup system. Two items are
backed up: the ``files/aegis.json`` file and the entire ``shared_prefs``
directory. The option is disabled by default and can only be enabled if
encryption is enabled as well.

I tested this with Local Transport and Seedvault. To test with Local Transport,
see: https://developer.android.com/guide/topics/data/testingbackup.
2021-01-16 16:19:25 +01:00
Alexander BakkerandGitHub 07c768893a Merge pull request #666 from alexbakker/separate-prefs
Split preferences into separate fragments
2021-01-16 15:51:26 +01:00
Alexander Bakker ef8c116bdc Split preferences into separate fragments
<img src="https://alexbakker.me/u/5phth0i1ga.png" width="300" />
2021-01-16 15:46:13 +01:00
Alexander BakkerandGitHub 260a3b9c78 Merge pull request #651 from alexbakker/replace-fab
Replace the FAB with a bottom sheet dialog
2021-01-16 15:39:31 +01:00
Alexander BakkerandGitHub a5ec7666ec Merge pull request #647 from alexbakker/more-tests
Add some more tests
2021-01-16 15:39:02 +01:00
Alexander BakkerandGitHub ff450c3edf Merge pull request #644 from alexbakker/remove-acc-name-opt
Remove the "search in account names" option and do so by default
2021-01-16 15:37:08 +01:00
Alexander BakkerandGitHub e6e179307d Merge pull request #663 from alexbakker/fix-650
Explicitly opt out of WebView usage statistics collection
2021-01-16 15:36:36 +01:00
Alexander Bakker 2c0068bea7 Release v1.4.2
Build / build (push) Canceled after 0s
2021-01-16 15:27:00 +01:00
Alexander BakkerandGitHub 6dcde38667 Merge pull request #664 from alexbakker/downgrade-zip4j
Downgrade zip4j to 2.6.0
2021-01-16 15:10:39 +01:00
Michael SchättgenandGitHub ec1913c77a Merge pull request #661 from alexbakker/fix-660
Use name() instead of toString() on StandardCharsets.UTF_8
2021-01-13 20:44:07 +01:00
Michael SchättgenandGitHub 3e40cc9c8f Merge pull request #655 from alexbakker/phonefactor-error
Show a clearer error message when encountering phonefactor:// URI's
2021-01-13 20:24:26 +01:00
Michael SchättgenandGitHub 40d1e22079 Merge pull request #643 from alexbakker/documentsui-error
Simplify the auto lock block logic and show an error if DocumentsUI is missing
2021-01-13 20:22:57 +01:00
Michael SchättgenandGitHub 38bbb60414 Merge pull request #652 from alexbakker/fix-authy-import
Don't rely on accountType to detect proprietary Authy tokens
2021-01-13 20:11:22 +01:00
Alexander Bakker 98d39ee399 Downgrade zip4j to 2.6.0
It looks like we broke the Authenticator Plus importer in
d660fbc6d1 by upgrading zip4j. So this patch
downgrades it again. I couldn't immediately find this issue in their issue
tracker, so it may be worth spending some time reporting this to them later.
2021-01-12 21:27:24 +01:00
Alexander Bakker e324f649ee Replace the FAB with a bottom sheet dialog
This also removes the dependency on ``com.getbase:floatingactionbutton``.
2021-01-10 14:49:24 +01:00
Alexander Bakker 4f8a0b9020 Add some more tests
This adds tests for all of the importers, a new scrypt test and some more OTP tests. More to come.
2021-01-10 14:47:27 +01:00
Alexander Bakker b78b22acbb Explicitly opt out of WebView usage statistics collection 2021-01-09 15:58:21 +01:00
Alexander Bakker 737b9da292 Use name() instead of toString() on StandardCharsets.UTF_8
Apparently toString() doesn't produce the same result on every platform.
2021-01-08 17:38:02 +01:00
Alexander BakkerandGitHub dea13f56f5 Merge pull request #659 from deadmeu/fix-fullscreen-styles
Add dark theme variants of the fullscreen style
2021-01-07 20:11:55 +01:00
deadmeu 1f0c51b45b Add dark theme variants of the fullscreen style 2021-01-08 00:27:16 +10:00
Alexander Bakker 466e296988 Fix indentation of feature list format of the F-Droid app description 2021-01-06 17:47:39 +01:00
Alexander Bakker fb17f48013 Fix feature list format of the F-Droid app description 2021-01-06 17:30:36 +01:00
Alexander Bakker f203eab3ef Update F-Droid app description 2021-01-06 16:28:56 +01:00
Alexander Bakker 1bd9cf3a3f Release v1.4.1
Build / build (push) Canceled after 0s
2021-01-04 18:59:47 +01:00
Michael SchättgenandGitHub a795015487 Merge pull request #626 from alexbakker/update-deps-2
Update dependencies
2021-01-03 23:55:09 +01:00
Alexander Bakker 0a6c89cd9d Show a clearer error message when encountering phonefactor:// URI's
Sometimes users get confused when they're trying to scan a Microsoft Authenticator QR code and think the error occurs because of a bug in Aegis.
2021-01-03 19:58:39 +01:00
Alexander Bakker 951241e95d Don't rely on accountType to detect proprietary Authy tokens 2021-01-02 14:55:26 +01:00
Alexander Bakker ddbe9ccfb7 Fix ADB command in CONTRIBUTING.md 2021-01-01 12:57:20 +01:00
Alexander Bakker 1d44e05866 Remove the "search in account names" option and do so by default 2020-12-27 20:34:04 +01:00
Alexander Bakker 4482a21a54 Simplify the auto lock block logic and show an error if DocumentsUI is missing 2020-12-27 20:24:40 +01:00
Alexander Bakker 15e28abecd Update dependencies
This also disables the Jetifier, because we don't need it anymore.
2020-12-26 16:49:30 +01:00
Alexander Bakker 1748e2221f Release v1.4
Build / build (push) Canceled after 0s
2020-12-26 15:58:18 +01:00
Alexander Bakker b1b4c021cf Update translations from Crowdin 2020-12-26 15:41:34 +01:00
Michael SchättgenandGitHub a27e3c6364 Merge pull request #642 from alexbakker/block-minimize-lock
Fix an issue where the app would lock when showing DocumentsUI
2020-12-26 15:36:48 +01:00
Michael SchättgenandGitHub 8f7b152af4 Merge pull request #641 from alexbakker/fix-593-extra
Use GoogleAuthInfo.parseSecret for secrets from Google Authenticator
2020-12-26 15:23:13 +01:00
Michael SchättgenandGitHub 39f18b79cb Merge pull request #636 from alexbakker/libsu-mount-ns
Run libsu commands inside the global mount namespace
2020-12-26 15:18:28 +01:00
Michael SchättgenandGitHub 9a81cccbe2 Merge pull request #623 from alexbakker/fixup-gauth-proto
Make the Google Authenticator Protobuf parser more complete
2020-12-26 15:18:00 +01:00
Michael SchättgenandGitHub 2c6be39f88 Merge pull request #634 from alexbakker/fix-package-query
Explicitly define which packages (apps) we want to query
2020-12-26 15:15:00 +01:00
Michael SchättgenandGitHub a6299ffb5f Merge pull request #627 from alexbakker/pass-popup-position
Display the password reminder popup on top instead of the bottom
2020-12-26 15:14:06 +01:00
Alexander Bakker 6e5a80a000 Fix an issue where the app would lock when showing DocumentsUI 2020-12-26 13:22:40 +01:00
Alexander Bakker f012c7ce22 Use GoogleAuthInfo.parseSecret for secrets from Google Authenticator 2020-12-26 12:25:43 +01:00
Alexander Bakker 805a9001e1 Run libsu commands inside the global mount namespace
This ensures we're able to access other app's internal storage directories using libsu when running on Android 11 or newer.
2020-12-20 13:53:34 +01:00
Alexander Bakker 884a1d07c4 Explicitly define which packages (apps) we want to query
This is necessary to be able to see if the app a user wants to import from is installed.
See: https://developer.android.com/about/versions/11/privacy/package-visibility
2020-12-19 13:21:21 +01:00
Alexander Bakker e9f6371885 Display the password reminder popup on top instead of the bottom
This prevents the Android's autofill popup from occluding our popup
2020-12-12 13:44:23 +01:00
Michael Schättgen 1f2e45fd12 Add GitHub action and remove Travis config 2020-12-12 11:32:50 +01:00
Alexander Bakker 6a5323b12b Make the Google Authenticator Protobuf parser more complete
I don't think Google Authenticator actually currently supports any of these
extra digit/algorithm options, but they're specified in the proto file, so we
should support them for completeness sake.
2020-12-07 21:28:27 +01:00
Alexander Bakker 735c086726 Release v1.4-beta2 2020-12-04 22:10:04 +01:00
Alexander Bakker d94be2420c Downgrade Android Gradle plugin to 4.0.1 to fix a crash 2020-12-04 22:07:07 +01:00
Alexander Bakker 0b6da5a93c Prevent file provider name collision (for debug builds) 2020-12-04 22:03:58 +01:00
Alexander Bakker d24e1471a7 Release v1.4-beta1 2020-12-04 20:49:51 +01:00
Alexander Bakker 7ca072a29c Update translations from Crowdin 2020-12-04 20:29:55 +01:00
Alexander Bakker d5ac447332 Update Android Gradle plugin to fix the build 2020-12-04 20:08:53 +01:00
Alexander BakkerandGitHub 0067c79dfa Merge pull request #618 from alexbakker/backup-thread
Move the creation of exports/backups to a background thread
2020-12-04 10:48:03 +01:00
Alexander Bakker 4e198d2556 Move the creation of exports/backups to a background thread
This writes exports/backups to the cache directory first and then hands off to a
background thread to pipe the file to SAF. I'm cheating a bit, but it's the
easiest fix with our current architecture. In the future, we should skip the
first step by making VaultManager itself thread-safe.
2020-12-03 22:16:35 +01:00
Michael SchättgenandGitHub f15a0018ef Merge pull request #605 from alexbakker/highlight-on-add
After adding a new entry, scroll to it and highlight it
2020-12-03 21:48:48 +01:00
Alexander Bakker d121139676 After adding a new entry, scroll to it and highlight it 2020-11-30 20:08:09 +01:00
Alexander BakkerandGitHub 98b246ad20 Merge pull request #616 from richardk85/richardk85-tap-to-reveal-min-1
"Tap to reveal": change min duration from 3 to 1
2020-11-24 17:03:26 +01:00
Jay bce0d2c044 "Tap to reveal": change min duration from 3 to 1 2020-11-24 14:26:16 +01:00
Michael SchättgenandGitHub bf91917745 Merge pull request #604 from alexbakker/export-plain
Improve the export functionality in numerous ways
2020-11-08 21:18:50 +01:00
Michael SchättgenandGitHub a91339de73 Merge pull request #603 from alexbakker/resize-qr-image
Resize images to a max res of 640x480 before scanning for QR codes
2020-11-07 21:06:11 +01:00
Michael SchättgenandGitHub 5d9de7ac32 Merge pull request #602 from alexbakker/qr-multi-intent
Combine ACTION_PICK and ACTION_GET_CONTENT when selecting an image to scan
2020-11-07 20:51:26 +01:00
Alexander Bakker 5f2529ea33 Improve the export functionality in numerous ways
This patch improves the export functionality in the following ways:
1. Allow setting a password even if the Aegis vault is not encrypted
2. Display a scary warning when exporting an unencrypted vault
3. Support exporting to a Google Authenticator URI file
4. Option to use Android's share mechanism

<img src="https://alexbakker.me/u/375oh146vz.png" width="300" />
2020-10-30 23:03:10 +01:00
Michael SchättgenandGitHub 61364b5542 Merge pull request #601 from alexbakker/importer-help
Display some help text in the importer selection dialog
2020-10-30 21:10:36 +01:00
Alexander Bakker 69196ddbcc Display some help text in the importer selection dialog 2020-10-30 16:11:53 +01:00
Alexander BakkerandGitHub b21b6dc750 Merge pull request #574 from michaelschattgen/feature/panic-trigger
Add panic trigger which deletes vault
2020-10-30 12:21:58 +01:00
Michael Schättgen e14a61dbf6 Add panic trigger which deletes vault
Add guardianproject fdroid signing key and minor review improvements

Add minor review fixes

Add panic trigger which deletes vault

Add guardianproject fdroid signing key and minor review improvements
2020-10-30 11:46:28 +01:00
Michael SchättgenandGitHub e8f94968c9 Merge pull request #600 from alexbakker/fix-593
Strip " " and "-" when parsing secrets
2020-10-29 22:08:58 +01:00
Michael SchättgenandGitHub 8b7e5d265b Merge pull request #592 from alexbakker/update-deps
Update dependencies
2020-10-29 22:03:26 +01:00
Alexander Bakker 22a70ac626 Resize images to a max res of 640x480 before scanning for QR codes
I'm not sure why this works, but ZXing seems to have trouble with high
resolution images. For example, scanning a 4032x3024 image with a QR code in it
fails, but scanning a resized version of that exact same image works.
2020-10-25 15:01:39 +01:00
Alexander Bakker dd83b74251 Combine ACTION_PICK and ACTION_GET_CONTENT when selecting an image to scan
Also, only scan for QR codes, not all bar codes that ZXing supports.
2020-10-25 14:48:56 +01:00
Alexander Bakker 5a9da45a8e Strip " " and "-" when parsing secrets 2020-10-24 14:25:10 +02:00
Alexander Bakker cac13b082e Bump target SDK to 30 and update dependencies 2020-10-22 15:11:45 +02:00
Alexander BakkerandGitHub 86c3c401ae Merge pull request #595 from orangenbaumblatt/feature/backup-successful-toast
Show toast message if backup was created successfully
2020-10-04 12:52:44 +02:00
orangenbaumblatt 33ff7da7af Show toast message if backup was created successfully 2020-10-04 01:11:21 +02:00
Michael SchättgenandGitHub 0af48f8bfa Merge pull request #591 from alexbakker/fix-pin-on-pass-change
Reset the PIN keyboard option when the password is changed
2020-09-22 16:35:17 +02:00
Alexander Bakker 01885e1fc6 Reset the PIN keyboard option when the password is changed
This fixes an issue where the PIN keyboard was shown after a password change,
regardless of whether it only contained digits or not.
2020-09-21 18:09:07 +02:00
Michael SchättgenandGitHub 52d2713482 Merge pull request #564 from alexbakker/improve-auto-lock
Improve auto lock and make it more customizable
2020-08-26 16:51:40 +02:00
Alexander Bakker d875cb6baa Improve auto lock and make it more customizable
This patch makes the auto lock option more customizable. Users can now choose a
combination of the following: Locking Aegis when
- The back button is pressed
- The app is minimized
- The device is locked

<img src="https://alexbakker.me/u/rlj4y2u8pk.png" width="300">
2020-08-26 14:20:09 +02:00
Alexander BakkerandGitHub 0cdb882991 Merge pull request #573 from michaelschattgen/feature/theme-improvements
Minor color improvements for dark themes
2020-08-16 19:06:10 +02:00
Michael Schättgen 31d382d6ad Minor color improvements for dark themes 2020-08-16 18:22:16 +02:00
Alexander BakkerandGitHub 141f491ca7 Merge pull request #571 from michaelschattgen/feature/wipe-entries
Add checkbox dialog to wipe vault before import
2020-08-16 17:58:21 +02:00
Michael SchättgenandGitHub c0bbe50996 Merge pull request #572 from alexbakker/fix-159
Prevent a crash on rotation while a ProgressDialogTask is still running
2020-08-16 15:59:13 +02:00
Michael SchättgenandGitHub 875c752f78 Merge pull request #570 from alexbakker/fix-double-add-crash
Prevent a crash caused by adding the same entry to the vault twice
2020-08-16 15:41:51 +02:00
Michael SchättgenandGitHub 1d00a2d8d6 Merge pull request #569 from alexbakker/slot-tests
Add tests for the Slot classes
2020-08-16 15:40:37 +02:00
Michael SchättgenandGitHub 14407412bf Merge pull request #568 from alexbakker/deep-link-tests
Add tests for deep links and fix a minor issue
2020-08-16 15:40:16 +02:00
Alexander Bakker bb2716f640 Prevent a crash on rotation while a ProgressDialogTask is still running
This patch ensures ProgressDialogs are dismissed when the ON_PAUSE event is fired.
2020-08-16 15:20:43 +02:00
Michael Schättgen 0f6b0dddbe Add checkbox dialog to wipe vault before import
Only show dialog when vault contains entries

Add improvements from review
2020-08-16 15:20:34 +02:00
Alexander Bakker d43e5e04c7 Prevent a crash caused by adding the same entry to the vault twice
This could happen in rare cases where writing the vault to disk failed.
2020-08-15 18:38:24 +02:00
Alexander Bakker 4c86e154c1 Add tests for the Slot classes 2020-08-15 18:07:34 +02:00
Alexander Bakker a45c834b9c Add UI tests for deep links 2020-08-15 17:10:37 +02:00
Alexander Bakker b746ca0c6e Ignore deep link intents if the given URI is null 2020-08-15 17:10:33 +02:00
Alexander BakkerandGitHub 7d38bc9b71 Merge pull request #567 from michaelschattgen/feature/group-manager-improvement
Add empty state for GroupManagerActivity
2020-08-13 19:18:49 +02:00
Alexander BakkerandGitHub 9557b60c93 Merge pull request #566 from michaelschattgen/feature/fix-fab
Fix hidden FAB when list changes
2020-08-13 19:05:31 +02:00
Michael Schättgen 227b5fd87c Improve GroupManagerActivity 2020-08-13 18:16:23 +02:00
Michael Schättgen 7d063104dd Fix hidden FAB when list changes 2020-08-13 13:06:20 +02:00
Michael SchättgenandGitHub c03d00695d Merge pull request #563 from alexbakker/fix-lint-errors
Replace use of android:tint with app:tint to fix lint errors
2020-08-12 17:32:04 +02:00
Michael SchättgenandGitHub 171c774b21 Merge pull request #562 from alexbakker/activity-test-rule
Don't use deprecated ActivityTestRule in UI tests
2020-08-12 17:31:52 +02:00
Alexander Bakker 97944aac88 Replace use of android:tint with app:tint to fix lint errors 2020-08-12 13:14:37 +02:00
Michael SchättgenandGitHub ca21e0bdd1 Merge pull request #561 from alexbakker/deps
Update Gradle and dependencies
2020-08-12 13:03:34 +02:00
Alexander Bakker 83c1914ddc Don't use deprecated ActivityTestRule in UI tests 2020-08-12 13:00:55 +02:00
Alexander Bakker d660fbc6d1 Update Gradle and dependencies 2020-08-12 12:54:13 +02:00
Alexander Bakker 35b5ce9d17 Release v1.3 2020-08-10 19:10:44 +02:00
Alexander Bakker 762fb9bccd Update translations from Crowdin 2020-08-10 19:08:47 +02:00
Alexander Bakker c3d38c18a3 Update the list of licenses of our dependencies 2020-08-10 18:49:53 +02:00
Michael SchättgenandGitHub 7a3ad11902 Merge pull request #558 from alexbakker/more-plurals
Convert pref_backups_versions_summary to a plurals definition
2020-08-08 12:33:18 +02:00
Alexander Bakker 28fcd68b72 Convert pref_backups_versions_summary to a plurals definition 2020-08-08 12:15:52 +02:00
Michael SchättgenandGitHub c2ed4b4849 Merge pull request #555 from alexbakker/update-contributing
Update contributing guidelines
2020-08-06 12:31:39 +02:00
Michael SchättgenandGitHub 9bbc2886c2 Merge pull request #503 from shihern/master
Improve usability of drag-and-drop feature
2020-08-06 12:13:03 +02:00
Connor Lim 5886462d2c Improve usability of drag-and-drop feature 2020-08-07 00:20:38 +08:00
Michael SchättgenandGitHub 98e8a0d97e Merge pull request #554 from alexbakker/plurals
Replace some string definitions with plurals definitions
2020-08-06 11:05:09 +02:00
Alexander Bakker 0368a992de Update contributing guidelines
This adds a blurb about translations to CONTRIBUTING.md and rewords some
sentences there. I also added a Contributing section to the README.
2020-08-05 16:44:37 +02:00
Alexander Bakker 233eeb6aa1 Replace some string definitions with plurals definitions 2020-08-05 13:28:23 +02:00
Alexander Bakker e5d162be9b Update translations (from Crowdin) 2020-08-05 12:00:08 +02:00
Alexander BakkerandGitHub 718d575a59 Merge pull request #552 from theRealPadster/master
Grammar fix (setup -> set up)
2020-08-04 20:22:02 +02:00
Isaac Maier 59e8c23a01 Grammar fix (setup -> set up) 2020-08-04 14:06:21 -04:00
Michael SchättgenandGitHub 847009d733 Merge pull request #548 from alexbakker/travis-disable-ui-tests
Disable UI tests on Travis CI
2020-08-03 23:37:44 +02:00
Alexander Bakker ce3d674da8 Disable UI tests on Travis CI
Unfortunately, the UI tests are just too unstable, so this patch excludes them
from being run on Travis. The tests run fine on our personal machines, but
produce seemingly random errors on Travis CI.
2020-08-02 21:32:03 +02:00
Alexander Bakker 9e4313e138 Release v1.3-beta2 2020-08-02 12:29:01 +02:00
Alexander Bakker 02e01723b2 Invert PIN keyboard preference if statement 2020-08-02 12:27:25 +02:00
Alexander Bakker 874f5e9058 Remove merge conflict marker from styles.xml 2020-08-01 19:50:18 +02:00
Alexander Bakker 7518408554 Release v1.3-beta1 2020-08-01 18:30:13 +02:00
Michael SchättgenandGitHub 6d68625ee2 Merge pull request #536 from alexbakker/new-intro
Replace AppIntro with a new custom intro
2020-08-01 14:11:35 +02:00
Alexander Bakker 0e78fd9652 Replace AppIntro with a new custom intro
This removes the dependency on AppIntro and replaces it with our own custom
intro implementation, backed by ViewPager2. We're doing this because we want a
more reliable and customizable onboarding for Aegis.

I've kept the design mostly the same as it was before, but tried to achieve a
bit of a cleaner look:

<img src="https://alexbakker.me/u/vsr3ahpjt6.png" width="200"> <img
src="https://alexbakker.me/u/efqid2ixly.png" width="200"> <img
src="https://alexbakker.me/u/oehmjm0rn9.png" width="200">
2020-08-01 14:09:58 +02:00
Michael SchättgenandGitHub 9d44d6abb2 Merge pull request #540 from alexbakker/fix-nav-bar
Set the correct navigation bar color pre API 27
2020-08-01 14:03:39 +02:00
Michael SchättgenandGitHub 42c308bc8f Merge pull request #545 from alexbakker/clarify-pass-reminder
Rephrase the password reminder message to be a bit clearer
2020-08-01 14:03:21 +02:00
Michael SchättgenandGitHub 20fb531f91 Merge pull request #526 from alexbakker/localize-everything
Make all remaining hardcoded strings translatable
2020-08-01 14:00:32 +02:00
Michael SchättgenandGitHub b8b3da4f85 Merge pull request #537 from alexbakker/support-proprietary-authy
Support importing proprietary tokens from Authy
2020-08-01 14:00:20 +02:00
Michael SchättgenandGitHub 7e403c2af6 Merge pull request #529 from alexbakker/fix-bioprompt-mixup
Fix behavior of AuthActivity under certain conditions
2020-08-01 13:34:09 +02:00
Michael SchättgenandGitHub be0529a0c8 Merge pull request #527 from alexbakker/fix-131
Automatically focus the EditText when showing text/password dialogs
2020-08-01 13:31:07 +02:00
Alexander Bakker 9c5f136f5b Make all remaining hardcoded strings translatable
I'm pretty sure I got all of them, but it's possible the linter missed some.
2020-08-01 13:14:49 +02:00
Michael SchättgenandGitHub 8c34ce8f38 Merge pull request #525 from alexbakker/icon-multi-intent
Combine ACTION_PICK and ACTION_GET_CONTENT when selecting an icon
2020-08-01 13:09:53 +02:00
Michael SchättgenandGitHub 3cdf31f940 Merge pull request #519 from alexbakker/theme-fixes
Fix two minor issues in themes
2020-08-01 13:08:38 +02:00
Alexander BakkerandGitHub 7059f3be74 Merge pull request #510 from michaelschattgen/feature/pin-keyboard
Add ability to enable PIN keyboard
2020-08-01 13:04:12 +02:00
Michael Schättgen 0c0e8c0d66 Add ability to enable PIN keyboard
Review fixes

Fix line
2020-08-01 13:03:18 +02:00
Alexander Bakker 06fd77b5f0 Rephrase the password reminder message to be a bit clearer 2020-07-30 21:52:16 +02:00
Alexander Bakker 9121922ef2 Set the correct navigation bar color pre API 27
Not very pretty, but I don't know of a better way.
2020-07-29 13:06:31 +02:00
Alexander Bakker 2347a322b5 Update Travis CI badge in README (again) 2020-07-28 22:49:40 +02:00
Alexander Bakker fcd3623eef Update Travis CI badge in README 2020-07-28 21:51:16 +02:00
Alexander Bakker 121ba06f5a Add a badge for the Matrix room to the README 2020-07-28 18:47:46 +02:00
Alexander Bakker 6611aa851c Refresh the README a bit 2020-07-28 11:11:44 +02:00
Alexander BakkerandGitHub c0b8b2c7ee Merge pull request #534 from linsui/trans
Update Simplified Chinese Translation
2020-07-25 11:44:33 +02:00
Alexander Bakker 34888767ed Support importing proprietary tokens from Authy 2020-07-24 10:53:59 +02:00
linsui 61f2e6e020 Update Simplified Chinese Translation 2020-07-19 15:00:36 +08:00
Alexander Bakker ba3e6203ec Fix behavior of AuthActivity under certain conditions
- Properly use 'inhibitBioPrompt'
- Don't show the biometric prompt when a password reminder is needed
2020-07-15 14:23:49 +02:00
Alexander Bakker 906d1b453a Automatically focus the EditText when showing text/password dialogs 2020-07-15 13:45:29 +02:00
Alexander Bakker a9ccf412e7 Combine ACTION_PICK and ACTION_GET_CONTENT when selecting an icon 2020-07-15 12:53:16 +02:00
Alexander Bakker 21b588dc8b Add Arabic to the language list and sort it alphabetically 2020-07-15 12:14:28 +02:00
Alexander BakkerandGitHub 90e5003418 Merge pull request #517 from nourserry/ar
Introducing Arabic language
2020-07-15 12:01:18 +02:00
Alexander BakkerandGitHub f8cf87aae9 Merge pull request #513 from LucasVhc/portuguese-translation
Add brazilian portuguese translation
2020-07-15 12:00:35 +02:00
Alexander BakkerandGitHub a77b308896 Merge pull request #521 from SagePtr/patch-1
Preserve backwards compatibility with Android < 7.0
2020-07-14 11:56:39 +02:00
Sage Pointer a25dbc5cdd Add desugaring for Android < 7 compatibility 2020-07-12 18:09:50 +02:00
Alexander Bakker 2aa88a2921 Fix two minor issues in themes
- Neatly map configured themes to styles
- Make the dark NoActionBar themes inherit from Dark/AMOLED
2020-07-11 18:42:44 +02:00
Nour Serry 7fedb5ab83 Introducing Arabic language
Intitial Arabic language support.
All strings translated
2020-07-11 11:28:10 +02:00
Alexander Bakker 0eb9a25687 Add changelog of 1.2.1 and bump versionCode 2020-07-10 21:28:56 +02:00
Lucas 330e88c270 Add brazilian portuguese translation 2020-07-08 02:38:38 -03:00
Michael SchättgenandGitHub 407d059076 Merge pull request #511 from alexbakker/fix-bar-loop
Fix an issue where the progress bar refresher ran in a tight loop
2020-07-06 10:01:44 +02:00
Alexander Bakker de3d0d8515 Fix an issue where the progress bar refresher ran in a tight loop
If the animator scale setting is set to 0, the progress bar refresher would run
in a tight loop. This issue was introduced by
93cc945a2c and caused the UI tests to fail.
2020-07-05 22:00:28 +02:00
Alexander BakkerandGitHub 57ef52d6ff Merge pull request #502 from alexbakker/double-activity-guard
Guard against starting multiple instances of IntroActivity or AuthActivity
2020-07-04 12:30:25 +02:00
Alexander Bakker 189bd0d749 Guard against starting multiple instances of IntroActivity or AuthActivity
Apparently onResume of MainActivity is sometimes called even if it's not the top
level activity, causing IntroActivity or AuthActivity to be launched twice. This
patch (uglily) prevents that from happening.
2020-07-04 12:21:27 +02:00
Michael SchättgenandGitHub 4ef8d9e826 Merge pull request #504 from alexbakker/fix-zero-div
Fix division by zero when calculating progress for large periods
2020-07-04 12:19:31 +02:00
Michael SchättgenandGitHub 7bb8d28b3b Merge pull request #499 from alexbakker/fix-bioprompt-some-more
Improve the lifecycle handling of BiometricPrompt some more
2020-07-04 12:10:53 +02:00
Michael SchättgenandGitHub 94ebc08097 Merge pull request #498 from alexbakker/slot-null
Allow changing the password if no password slot is present
2020-07-04 12:05:26 +02:00
Michael SchättgenandGitHub a3e14c57e4 Merge pull request #493 from alexbakker/fix-export-filename-again
Fix export filename (again)
2020-07-04 12:04:40 +02:00
Michael SchättgenandGitHub b55bdf8f04 Merge pull request #489 from alexbakker/fix-keystore-catch
Fix catching ProviderException with KeyStoreException as the cause
2020-07-04 12:04:13 +02:00
Michael SchättgenandGitHub 3e0a97dbe0 Merge pull request #488 from alexbakker/explain-backups
Clarify that backups are automatic in the preference summary
2020-07-04 12:03:38 +02:00
Michael SchättgenandGitHub 9b13a56002 Merge pull request #487 from alexbakker/transfer-entry-buttons
Make the buttons in TransferEntriesActivity behave like actual buttons
2020-07-04 12:03:12 +02:00
Michael SchättgenandGitHub 32c145b02d Merge pull request #481 from alexbakker/no-singletask
Unset "singleTask" as the launchMode for MainActivity
2020-07-04 12:02:49 +02:00
Michael SchättgenandGitHub 25b359a136 Merge pull request #474 from alexbakker/fix-sql-import
Fix the flakiness of importing from SQLite databases
2020-07-04 11:57:23 +02:00
Michael SchättgenandGitHub 0eb338a0bf Merge pull request #464 from alexbakker/ui-tests
Add initial set of UI tests
2020-07-04 11:54:34 +02:00
Michael SchättgenandGitHub b59a29cff4 Merge pull request #462 from alexbakker/better-rtl
Improve RTL support
2020-07-04 11:44:43 +02:00
Michael SchättgenandGitHub c252e77c5b Merge pull request #461 from alexbakker/better-scanner
Replace barcodescanner with CameraX and ZXing
2020-07-04 11:34:59 +02:00
Alexander Bakker 44ca712186 Improve RTL support
This addresses most of the issues reported in #418. The intro is still broken, however.

<img src="https://alexbakker.me/u/m9wfihdw4a.png" width="300"/>
2020-07-04 11:31:38 +02:00
Alexander Bakker 93cc945a2c Fix division by zero when calculating progress for large periods 2020-07-01 16:16:06 +02:00
Alexander Bakker 68d6c39dd4 Improve the lifecycle handling of BiometricPrompt some more
This is a different take on what db681273e6 was
trying to accomplish, with the additional benefit that the prompt will now no
longer be shown on auto-lock.
2020-06-29 21:51:34 +02:00
Alexander Bakker 3a5a185940 Allow changing the password if no password slot is present 2020-06-29 21:25:49 +02:00
Alexander Bakker 3d351ece05 Fix export filename (again)
I managed to clobber the filename of exports with fec2f3ff94.
2020-06-25 16:24:49 +02:00
Alexander Bakker c65ed16790 Replace barcodescanner with CameraX and ZXing
This removes the dependency on ``me.dm7.barcodescanner:zxing`` and replaces it
with our own QR code scanner implementation using CameraX and ZXing. The main
reason for this change is to hopefully get better compatibility with obscure
devices. The barcodescanner library we were previously using seems unmaintained,
while Google is apparently putting a lot of effort into CameraX.

ScannerActivity has been almost entirely rewritten, but the functionality is
exactly the same as before.
2020-06-23 21:54:23 +02:00
Alexander Bakker 7c147befcb Fix catching ProviderException with KeyStoreException as the cause
We're looking for android.security.KeyStoreException as the cause of
ProviderException, not java.security.KeyStoreException. Unfortunately this is a
hidden class, so all we can do is check the class name.
2020-06-23 21:40:52 +02:00
Alexander BakkerandGitHub c73c29d2ec Merge pull request #470 from Vstory/patch-3
Update Chinese translation
2020-06-22 21:27:24 +02:00
VstoryandAlexander Bakker df0c6b5fa8 Update Chinese translation 2020-06-22 21:24:08 +02:00
Alexander Bakker 2ae7ec33f1 Clarify that backups are automatic in the preference summary 2020-06-22 21:06:41 +02:00
Alexander BakkerandGitHub 65beeffa93 Merge pull request #486 from michaelschattgen/feature/hidden-text-change
Change hidden text to the 'Black circle' character
2020-06-22 20:27:34 +02:00
Alexander Bakker b5fa6192b3 Make the buttons in TransferEntriesActivity behave like actual buttons 2020-06-22 20:06:55 +02:00
Michael Schättgen 721a3d2d27 Change hidden text to the 'Black circle' character 2020-06-22 18:06:06 +02:00
Alexander BakkerandGitHub bf9173301c Merge pull request #442 from orangenbaumblatt/feature/password-strength
Add password strength meter
2020-06-22 13:21:44 +02:00
Alexander Bakker e2cf6a40cb Unset "singleTask" as the launchMode for MainActivity
We don't actually need this, and I have a feeling that it may be causing some of
the inexplicable crashes we're seeing in the Play Console. For example:
apparently the app sometimes gets itself into a state where the vault is
unlocked, but the user is still shown AuthActivity (possibly due to it being
launched twice). I can't prove that "singleTask" causes this, as I can't
reproduce the issue on my device or an emulator, but it's the only odd thing we
have in our activity lifecycle handling.

Test plan:
- Go through the intro, add an entry, change some settings, etc
- See if the app shortcuts still work. Scenarios:
    - The app is terminated.
    - The app is locked.
    - MainActivity is open.
    - Some other activity is open.
- See if auto-locking still works. Scenarios:
    - The app is locked.
    - MainActivity is open.
    - Some other activity is open.
- Turn on "Don't keep activities" in developer options and repeat the above
  steps.
2020-06-19 19:32:49 +02:00
Michael SchättgenandGitHub db681273e6 Merge pull request #478 from alexbakker/fix-bioprompt-oneplus
Let Android handle the lifecycle of BiometricPrompt
2020-06-18 22:47:27 +02:00
Alexander Bakker 291fd5427b Let Android handle the lifecycle of BiometricPrompt
We previously stopped/started the biometric prompt every time in
onPause/onResume, but that's apparently not necessary (and discouraged according
to the documentation). This caused issues where the prompt would get stuck on
some devices. While working on this I ran into another issue where AuthActivity
was closed and reopened for no reason after rotation of the device, compounding
the issue. This patch also fixes that.
2020-06-18 17:21:08 +02:00
Alexander Bakker 362f1da11f Fix the flakiness of importing from SQLite databases
This patch ensures that we also copy over any files related to the SQLite
database (databases-shm, databases-wal, databases-journal) when copying it from
an app's internal storage.
2020-06-17 11:24:23 +02:00
orangenbaumblatt cc33c63501 Add password strength meter
String refactor


Layout improvements


Layout improvements


Fixes after rebuild
2020-06-16 22:50:17 +02:00
Alexander Bakker 6e54497492 Release v1.2 2020-06-16 17:31:04 +02:00
Alexander BakkerandGitHub 9fde453ead Merge pull request #472 from michaelschattgen/feature/fix-menu-title
Fix menu item title for transfer
2020-06-16 13:54:04 +02:00
Michael Schättgen 7cd48f1c09 Fix menu item title for transfer 2020-06-16 13:35:54 +02:00
Michael SchättgenandGitHub 4c3d277fce Merge pull request #465 from alexbakker/backup-error-bar
Improve backup error handling and frequency
2020-06-16 09:52:37 +02:00
Michael SchättgenandGitHub 0111626d80 Merge pull request #467 from alexbakker/fix-refresh-issue
Fix an issue where codes were not refreshed
2020-06-16 09:20:17 +02:00
Alexander Bakker 0b83546db7 Fix an issue where codes were not refreshed
Sometimes I get so enthusiastic when refactoring that I accidentally remove too
much. This issue was introduced in 98a38b03e4
2020-06-14 20:50:18 +02:00
Alexander Bakker 08ab8237e7 Improve backup error handling and frequency
This patch improves our backup functionality in a number of ways:
- Only backup the vault when important changes are made, not when the order of
  entries is changed, for instance.
- Don't bubble up backup errors when saving the vault.
- Instead, show an error bar in the main view if the most recent backup attempt
  failed.

<img src="https://alexbakker.me/u/kbhhj2hcgx.png" width="300" />

Clicking on the error bar will take the user to the backup settings.
2020-06-14 20:39:09 +02:00
Alexander Bakker 39ecfba3e4 Add initial set of UI tests
This patch adds an initial set of UI tests for Aegis built using Espresso. It
covers a fair bit of the essential functionality of the app, but there are lots
more tests we could add later on.

This also reconfigures our Travis CI build manifest to run the tests on API 21,
23, 27 and 28 emulators. It was a real pain to get this to work well, but let's
hope it's stable now.

I had to downgrade ``com.google.android.material`` to 1.0.0, because 1.1.0
introduced an issue where the test would hang.
2020-06-10 16:15:52 +02:00
Alexander Bakker ae5502b650 Release v1.2-beta5 2020-06-10 14:59:22 +02:00
Michael SchättgenandGitHub 821d0b5147 Merge pull request #460 from alexbakker/fix-intro-back
Disable wizard mode when the done slide of the intro is reached
2020-06-10 10:51:35 +02:00
Alexander Bakker b4ed1f35ce Disable wizard mode when the done slide of the intro is reached
This fixes a number of issues that can happen when navigating back to previous
slides, by disabling back navigation entirely. One such issue is that when
navigating back from the last slide, one would always land on the
password/biometrics setup slide, even if "none" was selected on the security
picker slide.
2020-06-10 09:59:37 +02:00
Alexander BakkerandGitHub a468342187 Merge pull request #455 from michaelschattgen/feature/lock-screen
Add lockscreen overhaul
2020-06-08 17:49:35 +02:00
Michael Schättgen 4f6035dc49 Add lockscreen overhaul 2020-06-08 10:35:44 +02:00
Michael SchättgenandGitHub 506adcf81f Merge pull request #459 from alexbakker/fix-371
Don't allow drag and drop if the entry list is filtered in any way
2020-06-07 23:33:21 +02:00
Michael SchättgenandGitHub 49c8c2d57d Merge pull request #458 from alexbakker/respect-anim-settings
Respect the global animator duration scale setting
2020-06-07 23:30:34 +02:00
Michael SchättgenandGitHub 2ab698c2d3 Merge pull request #457 from alexbakker/rename-slides
Rename the security slides of the intro
2020-06-07 23:09:07 +02:00
Alexander Bakker 770f5f9724 Rename the security slides of the intro
These names make it a bit more clear what each slide is for
2020-06-07 18:16:00 +02:00
Alexander Bakker 992dcd651b Don't allow drag and drop if the entry list is filtered in any way 2020-06-07 17:18:19 +02:00
Alexander Bakker 98a38b03e4 Respect the global animator duration scale setting
This changes the way the TOTP progress bar works, so that we can respect the
global animator duration scale setting.
2020-06-07 17:02:59 +02:00
Michael SchättgenandGitHub 626995ec91 Merge pull request #445 from alexbakker/fix-reminder
Fix an issue where the password reminder popup was not shown
2020-06-06 13:51:53 +02:00
Michael SchättgenandGitHub 00b0c8deaf Merge pull request #439 from alexbakker/copy-on-tap-option
Add an option to copy tokens on tap (and disable it by default)
2020-06-06 13:51:44 +02:00
Michael SchättgenandGitHub 8e5b51c42f Merge pull request #454 from alexbakker/fix-andotp-iters
Fix importing andOTP backups with more than 10000 PBKDF iterations
2020-06-06 13:50:33 +02:00
Alexander Bakker b81ec0f073 Fix importing andOTP backups with more than 10000 PBKDF iterations
These larger numbers of iterations cause the key derivation process to take
longer, so I also moved that task to the background. andOTP now also has a
proper "issuer" field, so we make use of that as well.

Also fixes an issue where padded base32 could not be decoded. This issue is only
present for the andOTP importer as far as I know, so that's why that change is
included here.
2020-06-06 13:44:23 +02:00
Alexander Bakker d49d42254b Add an option to copy tokens on tap (and disable it by default) 2020-06-06 12:42:27 +02:00
Michael SchättgenandGitHub 95d5b4afa1 Merge pull request #437 from alexbakker/fix-copy-anim
Fix issue where the copy animation continued after view holder rebind
2020-06-06 12:24:26 +02:00
Alexander BakkerandGitHub cca8c9bdf7 Merge pull request #428 from michaelschattgen/feature/qr-transfer
Add ability to transfer tokens with qr codes
2020-06-06 12:24:14 +02:00
Michael Schättgen 4e00191039 Add ability to transfer tokens with qr codes
Add minor improvements

Add label to activity
2020-06-06 12:23:28 +02:00
Michael SchättgenandGitHub dd09595866 Merge pull request #435 from alexbakker/fix-363
Don't add new entries to the view if restoring from instance state
2020-06-06 12:22:47 +02:00
Michael SchättgenandGitHub b27bfc3fa9 Merge pull request #432 from alexbakker/fix-252
Improve icon editing flow
2020-06-06 11:56:54 +02:00
Michael SchättgenandGitHub c6a9716d16 Merge pull request #431 from alexbakker/update-deps-again
Update dependencies and sort them alphabetically
2020-06-06 11:54:12 +02:00
Michael SchättgenandGitHub acd7a54ee9 Merge pull request #430 from orangenbaumblatt/feature/improve-action-bar-consistency
Improve Action Bar consistency for Settings and About
2020-06-04 01:29:44 +02:00
Alexander BakkerandGitHub e7d240549e Merge pull request #452 from lotusexpeditor/master
Add Turkish Translation
2020-06-02 11:25:27 +02:00
lotusexpeditorandGitHub 70fedc53b9 Add Turkish Translation 2020-06-02 04:13:56 +03:00
Alexander Bakker cc163f952f Fix an issue where the password reminder popup was not shown
We no longer create BiometricPrompt prematurely, but forgot to adjust the check for the password reminder.
2020-05-28 11:55:56 +02:00
Alexander Bakker 63f57667ba Fix issue where the copy animation continued after view holder rebind 2020-05-25 12:02:21 +02:00
Alexander Bakker 34718a7b89 Don't add new entries to the view if restoring from instance state
Also addresses other potential issues with the same cause. Fixes #363.
2020-05-24 20:24:04 +02:00
orangenbaumblatt 6ac91e6cf3 Improve Action Bar consistency for Settings and About
Change Group Manager back icon
2020-05-24 15:10:59 +02:00
Alexander Bakker a39d6ab346 Improve icon editing flow
This improves the entry icon editing flow as suggested in #252:
- Add an "Edit icon" menu item
- Save the icon even if the checkmark was not clicked
- Exit icon edit mode with the back button

Close #252.
2020-05-24 13:49:30 +02:00
Alexander Bakker 91d89670d2 Update dependencies and sort them alphabetically 2020-05-24 12:52:02 +02:00
328 changed files with 19803 additions and 5185 deletions
+3 -3
View File
@@ -11,9 +11,9 @@ guidelines](/CONTRIBUTING.md#bug-reports) before submitting an issue. -->
* __Version__:
* __Source__: (Google Play/GitHub/F-Droid/?)
* __Vault encrypted:__ Yes (with biometric unlock)/Yes/No
* __Device:__
* __Android version and ROM:__
* __Vault encrypted__: Yes (with biometric unlock)/Yes/No
* __Device__:
* __Android version and ROM__:
###### Steps to reproduce
+12
View File
@@ -0,0 +1,12 @@
name: build
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v2
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e2c57acffb2c9aa5a8dc6eda2bbae0b6e495bd4c
- name: Build the app
run: ./gradlew build
-15
View File
@@ -1,15 +0,0 @@
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
+29 -13
View File
@@ -1,25 +1,40 @@
# Contributing
There are a couple of ways to contribute to Aegis. This document contains some
general guidelines for each type of contribution.
Looking to contribute to Aegis? That's great! There are a couple of ways to help
out. This document contains some general guidelines for each type of
contribution.
## Translations
We use [Crowdin](https://crowdin.com/project/aegis-authenticator) to crowdsource
translations of Aegis for lots of different languages. __Pull requests that
add/update a translation are no longer accepted.__ Crowdin is our single source
of truth for translations, to keep things easy to maintain.
The top 30 languages are available for translation. It's possible that yours is
not in that list. If that's the case, feel free to send us an email and we'll
add it.
## 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.
discuss it with us first through [a
proposal](https://github.com/beemdevelopment/Aegis/issues/new?labels=proposal&template=feature.md)
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
We use GitHub's issue tracker to track bugs. To make bug reports easier to
follow up on for us, they 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.
template and does not contain enough information, it will be closed. 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.
Please consider trying to find the root cause yourself first and include your
analysis of the issue in your report. Perhaps even send us a patch that fixes
it! We're happy to help you if you get stuck along the way.
### Capturing a log with ADB
@@ -81,7 +96,7 @@ __On your PC__:
```
adb logcat -c
adb logcat -f debug.log
adb logcat > debug.log
```
The logcat command captures the full system log by default, which may expose
@@ -91,8 +106,9 @@ __On your PC__:
one below:
```sh
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) -f debug.log
adb logcat --pid=$(adb shell pidof -s com.beemdevelopment.aegis) > debug.log
```
_If you are using a debug APK, replace ``com.beemdevelopment.aegis`` with
``com.beemdevelopment.aegis.debug``._
+55 -46
View File
@@ -1,45 +1,46 @@
# Aegis
<img align="left" width="80" height="80" src="metadata/en-US/images/icon.png"
alt="App icon">
Aegis is a free, secure and open source 2FA app for Android.
# Aegis Authenticator [![CI](https://github.com/beemdevelopment/Aegis/workflows/build/badge.svg)](https://github.com/beemdevelopment/Aegis/actions?query=workflow%3Abuild) [![Crowdin](https://badges.crowdin.net/aegis-authenticator/localized.svg)](https://crowdin.com/project/aegis-authenticator) [![Donate](https://img.shields.io/badge/donate-buy%20us%20a%20beer-%23FF813F)](https://www.buymeacoffee.com/beemdevelopment) [![Matrix](https://img.shields.io/badge/chat-Matrix-blue)](https://matrix.to/#/#aegis:matrix.org)
Aegis' security design and vault format is described in detail in [this
document](docs/vault.md).
__Aegis Authenticator__ is a free, secure and open source 2FA app for Android.
It aims to provide a secure authenticator for your online services, while also
including some features missing in existing authenticator apps, like proper
encryption and backups. Aegis supports HOTP and TOTP, making it compatible with
thousands of services.
# Features
The security design of the app and the vault format is described in detail in
[this document](docs/vault.md).
## Features
- Free and open source
- Secure
- Encryption (AES-256)
- The vault is encrypted (AES-256-GCM), and can be unlocked with:
- Password (scrypt)
- Biometrics (Android Keystore)
- Screen capture prevention
- Tap to reveal ability
- Multiple ways to add new entries
- Scan QR code
- Enter details manually
- 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)
- Tap to reveal
- Compatible with Google Authenticator
- Supports industry standard algorithms:
[HOTP](https://tools.ietf.org/html/rfc4226) and
[TOTP](https://tools.ietf.org/html/rfc6238)
- Lots of ways to add new entries
- Scan a QR code or an image of one
- Enter details manually
- Import from other authenticator apps: 2FAS Authenticator, Authenticator
Plus, Authy, andOTP, FreeOTP, FreeOTP+, Google Authenticator, Microsoft
Authenticator, Plain text, Steam, TOTP Authenticator and WinAuth (root
access is required for some of these)
- Organization
- Custom or default icons
- Drag and drop
- Custom groups
- Alphabetic/custom sorting
- Custom or automatically generated icons
- Group entries together
- Advanced entry editing
- Material design with multiple themes:
- Light theme
- Dark theme
- Amoled / true dark theme
- Search by name/issuer
- Material design with multiple themes: Light, Dark, AMOLED
- Export (plaintext or encrypted)
- Automatic backups of the vault to a location of your choosing
## Screenshots
@@ -102,28 +103,36 @@ Certificate fingerprints:
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
### Icon packs
- [aegis-icons](https://github.com/krisu5/aegis-icons)
Aegis supports icon packs to make it easier to assign icons to the entries in
your vault. There are no official icon packs, but the community maintains a
number of third-party icon packs you may want to check out. To learn how to
create your own Aegis-compatible icon pack, see [the
documenation](docs/iconpacks.md).
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.
- [aegis-icons](https://github.com/aegis-icons/aegis-icons)
[<img width=500 alt="Aegis-icons preview"
src="https://raw.githubusercontent.com/krisu5/aegis-icons/master/showcase.png">](https://github.com/krisu5/aegis-icons)
Unofficial monochrome-styled 2FA icons.
## Libraries
[<img width=500 alt="aegis-icons preview"
src="https://raw.githubusercontent.com/aegis-icons/aegis-icons/master/showcase.png">](https://github.com/aegis-icons/aegis-icons)
- [TextDrawable](https://github.com/amulyakhare/TextDrawable) by Amulya Khare
- [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
- [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
- [aegis-simple-icons](https://github.com/alexbakker/aegis-simple-icons)
This project periodically generates an icon pack for Aegis based on [Simple
Icons](https://simpleicons.org/). The icons are automatically generated, so
not all of them are as high quality as the ones you'll find in
[aegis-icons](https://github.com/aegis-icons/aegis-icons).
## Contributing
Looking to contribute to Aegis? That's great! There are a couple of ways to help
out. Translations, bug reports and pull requests are all greatly appreciated.
Please refer to our [contributing guidelines](CONTRIBUTING.md) to get started.
Swing by our Matrix room to interact with other contributors:
[#aegis:matrix.org](https://matrix.to/#/#aegis:matrix.org).
## License
+102 -41
View File
@@ -13,18 +13,25 @@ def getCmdOutput = { cmd ->
def getGitHash = { -> return getCmdOutput(["git", "rev-parse", "--short", "HEAD"]) }
def getGitBranch = { -> return getCmdOutput(["git", "rev-parse", "--abbrev-ref", "HEAD"]) }
def packageName = "com.beemdevelopment.aegis"
def fileProviderAuthority = "${packageName}.fileprovider"
def fileProviderAuthorityDebug = "${packageName}.debug.fileprovider"
android {
compileSdkVersion 29
compileSdkVersion 30
defaultConfig {
applicationId "com.beemdevelopment.aegis"
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 29
versionCode 33
versionName "1.2-beta4"
targetSdkVersion 30
versionCode 48
versionName "2.0"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
@@ -33,20 +40,36 @@ android {
}
testOptions {
unitTests.all {
useJUnitPlatform()
execution 'ANDROIDX_TEST_ORCHESTRATOR'
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
unitTests {
all {
maxHeapSize "3g"
ignoreFailures false
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
}
}
includeAndroidResources true
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
manifestPlaceholders = [title: "AegisDev", iconName: "ic_launcher_debug"]
manifestPlaceholders = [
title: "AegisDev",
iconName: "ic_launcher_debug",
fileProviderAuthority: "${fileProviderAuthorityDebug}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthorityDebug}\"")
resValue "bool", "pref_secure_screen_default", "false"
postprocessing {
removeUnusedCode true
@@ -57,7 +80,12 @@ android {
}
}
release {
manifestPlaceholders = [title: "Aegis", iconName: "ic_launcher"]
manifestPlaceholders = [
title: "Aegis",
iconName: "ic_launcher",
fileProviderAuthority: "${fileProviderAuthority}"
]
buildConfigField("String", "FILE_PROVIDER_AUTHORITY", "\"${fileProviderAuthority}\"")
resValue "bool", "pref_secure_screen_default", "true"
postprocessing {
removeUnusedCode true
@@ -72,6 +100,7 @@ android {
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
}
@@ -91,37 +120,69 @@ protobuf {
}
dependencies {
def libsuVersion = '2.5.1'
def androidTestVersion = '1.3.0'
def cameraxVersion = '1.0.0'
def glideVersion = '4.12.0'
def guavaVersion = '30.1.1'
def junitVersion = '4.13.2'
def libsuVersion = '3.1.2'
annotationProcessor 'androidx.annotation:annotation:1.2.0'
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.protobuf:protobuf-javalite:3.8.0'
implementation 'net.lingala.zip4j:zip4j:2.5.2'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.biometric:biometric:1.0.1"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.camera:camera-camera2:$cameraxVersion"
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-view:1.0.0-alpha24"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.guava:guava:28.2-android'
implementation 'com.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:6.0.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
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") {
implementation "androidx.core:core:1.3.2"
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.lifecycle:lifecycle-process:2.3.1"
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.github.avito-tech:krop:0.52'
implementation "com.github.bumptech.glide:annotations:${glideVersion}"
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation("com.github.bumptech.glide:recyclerview-integration:${glideVersion}") {
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 'com.google.guava:guava:28.2-jre"'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
implementation "com.google.guava:guava:${guavaVersion}-android"
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.protobuf:protobuf-javalite:3.16.0'
implementation 'com.google.zxing:core:3.4.1'
implementation "com.mikepenz:iconics-core:3.2.5"
implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.5@aar'
implementation 'com.nulab-inc:zxcvbn:1.5.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
// NOTE: this is kept at an old version on purpose (something in newer versions breaks the Authenticator Plus importer)
implementation 'net.lingala.zip4j:zip4j:2.6.0'
implementation 'info.guardianproject.trustedintents:trustedintents:0.2'
implementation 'org.bouncycastle:bcprov-jdk15on:1.68'
androidTestImplementation "androidx.test:core:${androidTestVersion}"
androidTestImplementation "androidx.test:runner:${androidTestVersion}"
androidTestImplementation "androidx.test:rules:${androidTestVersion}"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
androidTestImplementation "junit:junit:${junitVersion}"
androidTestUtil 'androidx.test:orchestrator:1.3.0'
testImplementation "androidx.test:core:${androidTestVersion}"
testImplementation "com.google.guava:guava:${guavaVersion}-jre"
testImplementation "junit:junit:${junitVersion}"
testImplementation "org.json:json:20210307"
testImplementation 'org.robolectric:robolectric:4.5.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
}
+3
View File
@@ -16,6 +16,9 @@
# public *;
#}
-keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; }
-keep class com.beemdevelopment.aegis.ui.fragments.*
-keep class com.beemdevelopment.aegis.importers.** { *; }
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
@@ -0,0 +1,113 @@
package com.beemdevelopment.aegis;
import android.view.View;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.platform.app.InstrumentationRegistry;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.crypto.SCryptParameters;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.vault.Vault;
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.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import org.hamcrest.Matcher;
import java.lang.reflect.InvocationTargetException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public abstract class AegisTest {
public static final String VAULT_PASSWORD = "test";
protected AegisApplication getApp() {
return (AegisApplication) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
}
protected VaultManager getVault() {
return getApp().getVaultManager();
}
protected VaultManager initVault() {
VaultFileCredentials creds = generateCredentials();
VaultManager vault = getApp().initVaultManager(new Vault(), creds);
try {
vault.save(false);
} catch (VaultManagerException e) {
throw new RuntimeException(e);
}
getApp().getPreferences().setIntroDone(true);
return vault;
}
protected VaultFileCredentials generateCredentials() {
PasswordSlot slot = new PasswordSlot();
byte[] salt = CryptoUtils.generateSalt();
SCryptParameters scryptParams = new SCryptParameters(
CryptoUtils.CRYPTO_SCRYPT_N,
CryptoUtils.CRYPTO_SCRYPT_r,
CryptoUtils.CRYPTO_SCRYPT_p,
salt
);
VaultFileCredentials creds = new VaultFileCredentials();
try {
SecretKey key = slot.deriveKey(VAULT_PASSWORD.toCharArray(), scryptParams);
slot.setKey(creds.getKey(), CryptoUtils.createEncryptCipher(key));
} catch (NoSuchAlgorithmException
| InvalidKeyException
| InvalidAlgorithmParameterException
| NoSuchPaddingException
| SlotException e) {
throw new RuntimeException(e);
}
creds.getSlots().add(slot);
return creds;
}
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
byte[] secret = CryptoUtils.generateRandomBytes(20);
OtpInfo info;
try {
info = type.getConstructor(byte[].class).newInstance(secret);
} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
return new VaultEntry(info, name, issuer);
}
// source: https://stackoverflow.com/a/30338665
protected static ViewAction clickChildViewWithId(final int id) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return null;
}
@Override
public String getDescription() {
return "Click on a child view with specified id.";
}
@Override
public void perform(UiController uiController, View view) {
View v = view.findViewById(id);
v.performClick();
}
};
}
}
@@ -0,0 +1,29 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.content.Context;
import android.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.runner.AndroidJUnitRunner;
import com.beemdevelopment.aegis.util.IOUtils;
public class AegisTestRunner extends AndroidJUnitRunner {
@Override
public void callApplicationOnCreate(Application app) {
Context context = app.getApplicationContext();
// clear internal storage so that there is no vault file
IOUtils.clearDirectory(context.getFilesDir(), false);
// clear preferences so that the intro is started from MainActivity
ApplicationProvider.getApplicationContext().getFilesDir();
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.clear()
.apply();
super.callApplicationOnCreate(app);
}
}
@@ -0,0 +1,65 @@
package com.beemdevelopment.aegis;
import android.content.Intent;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static junit.framework.TestCase.assertTrue;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest extends AegisTest {
@Before
public void before() {
initVault();
}
@Test
public void doDeepLinkIntent() {
VaultEntry entry = generateEntry(TotpInfo.class, "Bob", "Google");
GoogleAuthInfo info = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
launch(info.getUri());
onView(withId(R.id.action_save)).perform(click());
VaultEntry createdEntry = (VaultEntry) getVault().getEntries().toArray()[0];
assertTrue(createdEntry.equivalates(entry));
}
@Test
public void doDeepLinkIntent_Empty() {
launch(null);
}
@Test
public void doDeepLinkIntent_Bad() {
launch(Uri.parse("otpauth://bad"));
onView(withId(android.R.id.button1)).perform(click());
}
@SuppressWarnings("deprecation")
private void launch(Uri uri) {
Intent intent = new Intent(getApp(), MainActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.setData(uri);
// we need to use the deprecated ActivityTestRule class because of https://github.com/android/android-test/issues/143
ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);
rule.launchActivity(intent);
}
}
@@ -0,0 +1,88 @@
package com.beemdevelopment.aegis;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.ui.IntroActivity;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
import static org.hamcrest.Matchers.not;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntroTest extends AegisTest {
@Rule
public final ActivityScenarioRule<IntroActivity> activityRule = new ActivityScenarioRule<>(IntroActivity.class);
@Test
public void doIntro_None() {
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
prev.check(matches(not(isDisplayed())));
next.perform(click());
onView(withId(R.id.rb_none)).perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
assertFalse(vault.isEncryptionEnabled());
assertNull(getVault().getCredentials());
}
@Test
public void doIntro_Password() {
ViewInteraction next = onView(withId(R.id.btnNext));
ViewInteraction prev = onView(withId(R.id.btnPrevious));
prev.check(matches(not(isDisplayed())));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD + "1"), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.text_password_confirm)).perform(replaceText(VAULT_PASSWORD), closeSoftKeyboard());
prev.perform(click());
prev.perform(click());
prev.check(matches(not(isDisplayed())));
next.perform(click());
next.perform(click());
next.perform(click());
next.perform(click());
VaultManager vault = getVault();
SlotList slots = getVault().getCredentials().getSlots();
assertTrue(vault.isEncryptionEnabled());
assertTrue(slots.has(PasswordSlot.class));
assertFalse(slots.has(BiometricSlot.class));
}
}
@@ -0,0 +1,187 @@
package com.beemdevelopment.aegis;
import androidx.annotation.IdRes;
import androidx.test.espresso.ViewInteraction;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.RootMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openContextualActionModeOverflowMenu;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNull;
import static junit.framework.TestCase.assertTrue;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class OverallTest extends AegisTest {
private static final String _groupName = "Test";
@Rule
public final ActivityScenarioRule<MainActivity> activityRule = new ActivityScenarioRule<>(MainActivity.class);
@Test
public void doOverallTest() {
ViewInteraction next = onView(withId(R.id.btnNext));
next.perform(click());
onView(withId(R.id.rb_password)).perform(click());
next.perform(click());
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
next.perform(click());
onView(withId(R.id.btnNext)).perform(click());
VaultManager vault = getVault();
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
List<VaultEntry> entries = Arrays.asList(
generateEntry(TotpInfo.class, "Frank", "Google"),
generateEntry(HotpInfo.class, "John", "GitHub"),
generateEntry(TotpInfo.class, "Alice", "Office 365"),
generateEntry(SteamInfo.class, "Gaben", "Steam")
);
for (VaultEntry entry : entries) {
addEntry(entry);
}
List<VaultEntry> realEntries = new ArrayList<>(vault.getEntries());
for (int i = 0; i < realEntries.size(); i++) {
String message = String.format("%s != %s", realEntries.get(i).toJson().toString(), entries.get(i).toJson().toString());
assertTrue(message, realEntries.get(i).equivalates(entries.get(i)));
}
for (int i = 0; i < 10; i++) {
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, clickChildViewWithId(R.id.buttonRefresh)));
}
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(0, longClick()));
onView(withId(R.id.action_copy)).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.action_edit)).perform(click());
onView(withId(R.id.text_name)).perform(clearText(), typeText("Bob"), closeSoftKeyboard());
onView(withId(R.id.dropdown_group)).perform(click());
onView(withText(R.string.new_group)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
onView(withId(R.id.text_input)).perform(typeText(_groupName), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
onView(isRoot()).perform(pressBack());
onView(withId(android.R.id.button1)).perform(click());
changeSort(R.string.sort_alphabetically_name);
changeSort(R.string.sort_alphabetically_name_reverse);
changeSort(R.string.sort_alphabetically);
changeSort(R.string.sort_alphabetically_reverse);
changeSort(R.string.sort_custom);
changeGroupFilter(_groupName);
changeGroupFilter(null);
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
onView(withId(R.id.action_share_qr)).perform(click());
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
onView(withId(R.id.action_delete)).perform(click());
onView(withId(android.R.id.button1)).perform(click());
openContextualActionModeOverflowMenu();
onView(withText(R.string.lock)).perform(click());
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.button_decrypt)).perform(click());
vault = getVault();
openContextualActionModeOverflowMenu();
onView(withText(R.string.action_settings)).perform(click());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_section_security_title)), click()));
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
onView(withId(android.R.id.button1)).perform(click());
assertFalse(vault.isEncryptionEnabled());
assertNull(vault.getCredentials());
onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItemAtPosition(1, click()));
onView(withId(R.id.text_password)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(R.id.text_password_confirm)).perform(typeText(VAULT_PASSWORD), closeSoftKeyboard());
onView(withId(android.R.id.button1)).perform(click());
assertTrue(vault.isEncryptionEnabled());
assertTrue(vault.getCredentials().getSlots().has(PasswordSlot.class));
}
private void changeSort(@IdRes int resId) {
onView(withId(R.id.action_sort)).perform(click());
onView(withText(resId)).perform(click());
}
private void changeGroupFilter(String text) {
onView(withId(R.id.chip_group)).perform(click());
if (text == null) {
onView(withId(R.id.btnClear)).perform(click());
} else {
onView(withText(text)).perform(click());
onView(isRoot()).perform(pressBack());
}
}
private void addEntry(VaultEntry entry) {
onView(withId(R.id.fab)).perform(click());
onView(withId(R.id.fab_enter)).perform(click());
onView(withId(R.id.accordian_header)).perform(click());
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
onView(withId(R.id.text_issuer)).perform(typeText(entry.getIssuer()), closeSoftKeyboard());
if (entry.getInfo().getClass() != TotpInfo.class) {
String otpType;
if (entry.getInfo() instanceof HotpInfo) {
otpType = "HOTP";
} else if (entry.getInfo() instanceof SteamInfo) {
otpType = "Steam";
} else if (entry.getInfo() instanceof TotpInfo) {
otpType = "TOTP";
} else {
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
}
onView(withId(R.id.dropdown_type)).perform(click());
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
}
String secret = Base32.encode(entry.getInfo().getSecret());
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
onView(withId(R.id.action_save)).perform(click());
}
}
@@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.vault;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.beemdevelopment.aegis.AegisTest;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VaultManagerTest extends AegisTest {
@Before
public void before() {
initVault();
}
@Test
public void testToggleEncryption() throws VaultManagerException {
getVault().disableEncryption();
assertFalse(getVault().isEncryptionEnabled());
assertNull(getVault().getCredentials());
VaultFileCredentials creds = generateCredentials();
getVault().enableEncryption(creds);
assertTrue(getVault().isEncryptionEnabled());
assertNotNull(getVault().getCredentials());
assertEquals(getVault().getCredentials().getSlots().findAll(PasswordSlot.class).size(), 1);
}
}
+68 -17
View File
@@ -6,28 +6,35 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<application
android:name=".AegisApplication"
android:allowBackup="false"
android:allowBackup="true"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backup_rules"
android:backupAgent=".AegisBackupAgent"
android:icon="@mipmap/${iconName}"
android:label="Aegis"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning"
android:theme="@style/Theme.Aegis.Launch"
tools:replace="android:theme">
<activity android:name=".ui.TransferEntriesActivity"
android:label="@string/title_activity_transfer" />
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about" />
<activity
android:name=".ui.SelectEntriesActivity"
android:label="Select entries" />
android:name=".ui.ImportEntriesActivity"
android:label="@string/title_activity_import_entries" />
<activity
android:name=".ui.MainActivity"
android:label="${title}"
android:launchMode="singleTask">
android:label="${title}">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -41,28 +48,72 @@
<data android:scheme="otpauth" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.ScannerActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="Scan a QR code"
android:label="@string/title_activity_scan_qr"
android:screenOrientation="portrait" />
<activity
android:name=".ui.EditEntryActivity"
android:label="Edit entry" />
android:label="@string/title_activity_edit_entry" />
<activity
android:name=".ui.IntroActivity"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.AuthActivity" />
<activity android:name=".ui.PreferencesActivity" />
android:screenOrientation="portrait" />
<activity
android:name=".ui.AuthActivity" />
<activity
android:name=".ui.PreferencesActivity"
android:label="@string/title_activity_preferences" />
<activity
android:name=".ui.SlotManagerActivity"
android:label="Manage key slots" />
android:label="@string/title_activity_manage_slots" />
<activity
android:name=".ui.GroupManagerActivity"
android:label="Manage groups" />
android:label="@string/title_activity_manage_groups" />
<activity
android:name=".ui.PanicResponderActivity"
android:launchMode="singleInstance"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="info.guardianproject.panic.action.TRIGGER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".ui.ExitActivity" />
<service android:name=".services.NotificationService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${fileProviderAuthority}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
</application>
</manifest>
<queries>
<package android:name="com.authy.authy" />
<package android:name="org.fedorahosted.freeotp" />
<package android:name="org.liberty.android.freeotpplus" />
<package android:name="com.google.android.apps.authenticator2" />
<package android:name="com.azure.authenticator" />
<package android:name="com.valvesoftware.android.steam.community" />
<package android:name="com.authenticator.authservice2" />
</queries>
</manifest>
+1
View File
@@ -0,0 +1 @@
../../../../LICENSE
+100
View File
@@ -31,6 +31,99 @@
</head>
<body>
<div></div>
<h3>Version 2.0</h3>
<h4>New</h4>
<ul>
<li>Support for icon packs</li>
<li>Support for participation in Android's backup system (Google Drive, Seedvault)</li>
<li>UI refresh (switched to the Material Components theme)</li>
<li>Bottom sheet with chips to filter on groups</li>
<li>Support for importing from 2FAS Authenticator</li>
<li>Search in account names by default (and remove the setting)</li>
<li>Replaced the FAB with a bottom sheet dialog</li>
<li>Reorganization of settings into separate categories</li>
<li>Ability to 'share' images of QR codes to scan in Aegis</li>
<li>Option to save the current group filter</li>
<li>New translations for Bulgarian, Danish, Latvian, Swedish and Ukranian</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>The QR code scanner had trouble detecting QR codes on some devices due to low resolution image capture</li>
<li>The app would vanish from the recent apps list after locking</li>
<li>When importing from Nextcloud, Aegis would report that the file could not be found.</li>
<li>The biometrics prompt would not appear on some devices</li>
<li>The app would lock when selecting a file/icon on certain devices and configurations</li>
<li>There were multiple layout issues on small screen devices</li>
<li>Various other usability, performance and stability improvements</li>
</ul>
<h3>Version 1.4.2</h3>
<h4>Fixes</h4>
<ul>
<li>The app would crash if DocumentsUI is not present on the device</li>
<li>The app would close when selecting an icon if auto lock on minimize was enabled</li>
<li>Importing from Authy was flaky for entries that have an icon</li>
<li>The dark theme was not properly applied to the QR code scanner view</li>
<li>The app would crash on plain text export on some devices</li>
<li>Importing from Authenticator Plus stopped working</li>
</ul>
<h3>Version 1.4.1</h3>
<h4>Fixes</h4>
<ul>
<li>Scanning QR codes stopped working on certain devices (primarily OnePlus)</li>
</ul>
<h3>Version 1.4</h3>
<h4>New</h4>
<ul>
<li>Optionally delete the vault if a panic trigger is received from Ripple</li>
<li>More customizable auto-lock</li>
<li>More flexible export options
<ul>
<li>Share mechanism</li>
<li>Offer to encrypt even if this feature is disabled in the app</li>
<li>Export to a Google Authenticator URI file</li>
</ul>
</li>
<li>Perform exports/backups on a background thread (automatic backups now work with Nextcloud)</li>
<li>Color improvements to the dark theme (slightly darker)</li>
<li>Offer more locations to select an image/icon from</li>
<li>Display some helpful information when importing from a different app</li>
<li>Minimum tap to reveal timeout changed to 1 second</li>
<li>After an entry is added, scroll to it and highlight it</li>
<li>Updated translations, and new translations for: Basque, Chinese Traditional, Hindi, Indonesian, Japanese, Persian, Romanian, Slovak</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Scanning large images for QR codes would fail</li>
<li>The FAB would remain hidden under certain circumstances</li>
<li>The app would crash if an entry was added to the vault twice due to an IO error</li>
<li>The app would crash if the device was rotated while a progress dialog was shown</li>
<li>The PIN keyboard would show even if a new non-digit password was set</li>
<li>The password reminder popup would be occluded by the autofill popup</li>
<li>Importing from other apps on Android 11 was broken due to some permission issues</li>
</ul>
<h3>Version 1.3</h3>
<h4>New</h4>
<ul>
<li>Completely rewritten intro/onboarding</li>
<li>Option to show a PIN keyboard when unlocking Aegis</li>
<li>A password strength meter when setting up encryption (based on zxcvbn)</li>
<li>RTL support</li>
<li>Arabic and Portuguese translations</li>
<li>Updates to existing translations</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Better lifecycle handling of the biometric authentication prompt</li>
<li>The filename of exported vaults had a double .json extension</li>
<li>The navigation bar color was incorrect on devices pre API 27</li>
<li>QR code scanner performance and stability improvements</li>
<li>Various other small usability and stability improvements</li>
</ul>
<h3>Version 1.2.1</h3>
<h4>Fixes</h4>
<ul>
<li>Fix a rare issue where the intro could end up in a bad state</li>
</ul>
<h3>Version 1.2</h3>
<h4>New</h4>
<ul>
@@ -41,6 +134,7 @@
<li>Add support for importing a plain text Google Authenticator URI file</li>
<li>Add support for importing from the new Google Authenticator export QR codes</li>
<li>Add support for otpauth://steam URI's</li>
<li>Add an option to copy tokens on tap (and disable it by default)</li>
<li>Improve method to notify users on copy</li>
<li>Add support for backups</li>
<li>Improve multiselect flow</li>
@@ -49,17 +143,23 @@
<li>Use most frequent period to show progress</li>
<li>Append a timestamp to the filename of exported vaults</li>
<li>Add Hungarian translation</li>
<li>Add Turkish translation</li>
<li>Display a warning if automatic time sync is not enabled</li>
<li>Minor card entry layout overhaul</li>
<li>Ability to transfer tokens with qr codes</li>
<li>Lockscreen overhaul</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Improve overall exception handling and error feedback to the user</li>
<li>Improve icon editing flow</li>
<li>Protect writes of the vault file against corruption with AtomicFile</li>
<li>Make the parsing logic of the QR code URI more robust</li>
<li>Importing from Authy now asks for password if needed</li>
<li>Update Russian localization</li>
<li>Increase password reminder period to 30 days</li>
<li>Fix importing andOTP backups with more than 10000 PBKDF iterations</li>
<li>Respect the global animator duration scale setting</li>
</ul>
<p>Various other minor improvements</p>
<h3>Version 1.1.4</h3>
+14
View File
@@ -0,0 +1,14 @@
<html>
<head>
<style type="text/css">
body {
background-color: %2$s;
color: %3$s;
font-size: 0.8em;
}
</style>
</head>
<body>
<pre>%1$s</pre>
</body>
</html>
@@ -12,10 +12,17 @@ import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.Vault;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
@@ -23,6 +30,7 @@ import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.material_design_iconic_typeface_library.MaterialDesignIconic;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import java.util.Collections;
@@ -33,15 +41,23 @@ public class AegisApplication extends Application {
private VaultManager _manager;
private Preferences _prefs;
private List<LockListener> _lockListeners;
private boolean _blockAutoLock;
private IconPackManager _iconPackManager;
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private static final String CODE_LOCK_VAULT_ACTION = "lock_vault";
static {
// to access other app's internal storage directory, run libsu commands inside the global mount namespace
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER));
}
@Override
public void onCreate() {
super.onCreate();
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
_iconPackManager = new IconPackManager(this);
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
@@ -53,6 +69,12 @@ public class AegisApplication extends Application {
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
registerReceiver(receiver, intentFilter);
// lock the app if the user moves the application to the background
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
// clear the cache directory on startup, to make sure no temporary vault export files remain
IOUtils.clearDirectory(getCacheDir(), false);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
@@ -77,7 +99,7 @@ public class AegisApplication extends Application {
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readFile(this);
_vaultFile = VaultManager.readVaultFile(this);
}
return _vaultFile;
@@ -107,12 +129,16 @@ public class AegisApplication extends Application {
return _manager;
}
public IconPackManager getIconPackManager() {
return _iconPackManager;
}
public Preferences getPreferences() {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
public boolean isAutoLockEnabled(int autoLockType) {
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
}
public void registerLockListener(LockListener listener) {
@@ -123,10 +149,25 @@ public class AegisApplication extends Application {
_lockListeners.remove(listener);
}
public void lock() {
/**
* Sets whether to block automatic lock on minimization. This should only be called
* by activities before invoking an intent that shows a DocumentsUI, because that
* action leads AppLifecycleObserver to believe that the app has been minimized.
*/
public void setBlockAutoLock(boolean block) {
_blockAutoLock = block;
}
/**
* Locks the vault and the app.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
public void lock(boolean userInitiated) {
_manager.destroy();
_manager = null;
for (LockListener listener : _lockListeners) {
listener.onLocked();
listener.onLocked(userInitiated);
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
@@ -141,6 +182,7 @@ public class AegisApplication extends Application {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
@@ -167,16 +209,31 @@ public class AegisApplication extends Application {
}
}
public class ScreenOffReceiver extends BroadcastReceiver {
private class AppLifecycleObserver implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP
&& isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
&& !_blockAutoLock) {
lock(false);
}
}
}
private class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
lock(false);
}
}
}
public interface LockListener {
void onLocked();
/**
* When called, the app/vault has been locked and the listener should perform its cleanup operations.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
void onLocked(boolean userInitiated);
}
}
@@ -0,0 +1,118 @@
package com.beemdevelopment.aegis;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.FullBackupDataOutput;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = BackupAgent.class.getSimpleName();
private Preferences _prefs;
@Override
public void onCreate() {
super.onCreate();
_prefs = new Preferences(this);
}
@Override
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// first copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (FileInputStream inStream = VaultManager.getAtomicFile(this).openRead();
FileOutputStream outStream = new FileOutputStream(vaultBackupFile)) {
IOUtils.copy(inStream, outStream);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir();
throw e;
}
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
throw e;
} finally {
deleteBackupDir();
}
Log.i(TAG, "onFullBackup() finished");
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
super.onRestoreFile(data, size, destination, type, mode, mtime);
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultManager.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
} finally {
deleteBackupDir();
}
}
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
}
@Override
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
super.onQuotaExceeded(backupDataBytes, quotaBytes);
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
}
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString()));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true);
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultManager.FILENAME);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}
@@ -7,11 +7,28 @@ import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class Preferences {
public static final int AUTO_LOCK_OFF = 1 << 0;
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
public static final int[] AUTO_LOCK_SETTINGS = {
AUTO_LOCK_ON_BACK_BUTTON,
AUTO_LOCK_ON_MINIMIZE,
AUTO_LOCK_ON_DEVICE_LOCK
};
private SharedPreferences _prefs;
public Preferences(Context context) {
@@ -26,14 +43,14 @@ public class Preferences {
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 isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
@@ -73,8 +90,25 @@ public class Preferences {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public boolean isAutoLockEnabled() {
return _prefs.getBoolean("pref_auto_lock", true);
return getAutoLockMask() != AUTO_LOCK_OFF;
}
public boolean isAutoLockTypeEnabled(int autoLockType) {
return (getAutoLockMask() & autoLockType) == autoLockType;
}
public void setAutoLockMask(int autoLock) {
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
}
public void setIntroDone(boolean done) {
@@ -128,7 +162,20 @@ public class Preferences {
}
}
return new Locale(lang);
String[] parts = lang.split("_");
if (parts.length == 1) {
return new Locale(parts[0]);
}
return new Locale(parts[0], parts[1]);
}
public boolean isAndroidBackupsEnabled() {
return _prefs.getBoolean("pref_android_backups", false);
}
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
}
public boolean isBackupsEnabled() {
@@ -160,6 +207,18 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setBackupsError(Exception e) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply();
}
public String getBackupsError() {
return _prefs.getString("pref_backups_error", null);
}
public boolean isPinKeyboardEnabled() {
return _prefs.getBoolean("pref_pin_keyboard", false);
}
public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true);
}
@@ -167,4 +226,31 @@ public class Preferences {
public void setIsTimeSyncWarningEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public boolean isCopyOnTapEnabled() {
return _prefs.getBoolean("pref_copy_on_tap", false);
}
public void setGroupFilter(List<String> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter", json.toString()).apply();
}
public List<String> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptyList();
}
try {
JSONArray json = new JSONArray(raw);
List<String> filter = new ArrayList<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.getString(i));
}
return filter;
} catch (JSONException e) {
return Collections.emptyList();
}
}
}
@@ -0,0 +1,29 @@
package com.beemdevelopment.aegis;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public class ThemeMap {
private ThemeMap() {
}
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Default,
Theme.DARK, R.style.Theme_Aegis_Dark_Default,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Default
);
public static final Map<Theme, Integer> NO_ACTION_BAR = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_NoActionBar,
Theme.DARK, R.style.Theme_Aegis_Dark_NoActionBar,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_NoActionBar
);
public static final Map<Theme, Integer> FULLSCREEN = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light_Fullscreen,
Theme.DARK, R.style.Theme_Aegis_Dark_Fullscreen,
Theme.AMOLED, R.style.Theme_Aegis_TrueDark_Fullscreen
);
}
@@ -63,9 +63,9 @@ public class KeyStoreHandle {
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by a KeyStoreException, throw a KeyStoreHandleException instead
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause instanceof KeyStoreException) {
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
public GuardianProjectFDroidRSA2048() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA4096 extends ApkSignaturePin {
public GuardianProjectFDroidRSA4096() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}
@@ -3,21 +3,19 @@ package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
public class Base32 {
private static final BaseEncoding _encoding = BaseEncoding.base32().omitPadding();
private Base32() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return _encoding.decode(s.toUpperCase());
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return _encoding.encode(data);
return BaseEncoding.base32().omitPadding().encode(data);
}
}
@@ -2,8 +2,8 @@ package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricConstants;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
public class BiometricsHelper {
private BiometricsHelper() {
@@ -12,16 +12,16 @@ public class BiometricsHelper {
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == 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;
return errorCode == BiometricPrompt.ERROR_CANCELED
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
@@ -0,0 +1,31 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
public class BitmapHelper {
private BitmapHelper() {
}
/**
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
*/
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
if (maxHeight <= 0 || maxWidth <= 0) {
return bitmap;
}
float maxRatio = (float) maxWidth / maxHeight;
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
int width = maxWidth;
int height = maxHeight;
if (maxRatio > 1) {
width = (int) ((float) maxHeight * ratio);
} else {
height = (int) ((float) maxWidth / ratio);
}
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
}
@@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.ContextWrapper;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import javax.annotation.Nullable;
/**
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
*/
public class ContextHelper {
private ContextHelper() {
}
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
@Nullable
public static ComponentActivity getActivity(@NonNull Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof ComponentActivity) {
return (ComponentActivity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
@Nullable
public static Lifecycle getLifecycle(@NonNull Context context) {
ComponentActivity activity = getActivity(context);
return activity == null ? null : activity.getLifecycle();
}
}
@@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.ArrayRes;
import com.beemdevelopment.aegis.R;
import java.util.List;
public class DropdownHelper {
private DropdownHelper() {
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
dropdown.setAdapter(adapter);
}
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
dropdown.setAdapter(adapter);
}
}
@@ -9,22 +9,10 @@ public class EditTextHelper {
private EditTextHelper() {
}
public static void clearEditText(EditText text) {
text.getText().clear();
}
public static char[] getEditTextChars(EditText text) {
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);
Editable editable = text.getText();
char[] chars = new char[editable.length()];
editable.getChars(0, editable.length(), chars, 0);
return chars;
}
@@ -6,7 +6,6 @@ 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;
@@ -18,6 +17,26 @@ public class FabScrollHelper {
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
setVisible(false);
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
setVisible(true);
}
}
public void setVisible(boolean visible) {
if (visible) {
_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();
} else {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
@@ -32,18 +51,7 @@ public class FabScrollHelper {
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();
}
}
}
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Build;
import android.widget.ImageView;
import com.beemdevelopment.aegis.icons.IconType;
public class IconViewHelper {
private IconViewHelper() {
}
/**
* Sets the layer type of the given ImageView based on the given IconType. If the
* icon type is SVG and SDK <= 27, the layer type is set to software. Otherwise, it
* is set to hardware.
*/
public static void setLayerType(ImageView view, IconType iconType) {
if (iconType == IconType.SVG && Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) {
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return;
}
view.setLayerType(ImageView.LAYER_TYPE_HARDWARE, null);
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.util.DisplayMetrics;
public class MetricsHelper {
private MetricsHelper() {
}
public static int convertDpToPixels(Context context, float dp) {
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
}
@@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import com.beemdevelopment.aegis.R;
public class PasswordStrengthHelper {
// Material design color palette
private static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
public static String getString(int score, Context context) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
String[] strings = context.getResources().getStringArray(R.array.password_strength);
return strings[score];
}
public static String getColor(int score) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
return COLORS[score];
}
}
@@ -0,0 +1,95 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.nio.ByteBuffer;
import static android.graphics.ImageFormat.YUV_420_888;
import static android.graphics.ImageFormat.YUV_422_888;
import static android.graphics.ImageFormat.YUV_444_888;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
public static final Size RESOLUTION = new Size(1200, 1600);
private final QrCodeAnalyzer.Listener _listener;
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
_listener = listener;
}
@Override
public void analyze(@NonNull ImageProxy image) {
int format = image.getFormat();
if (format != YUV_420_888 && format != YUV_422_888 && format != YUV_444_888) {
Log.e(TAG, String.format("Expected YUV format, got %d instead", format));
image.close();
return;
}
byte[] data = getLuminancePlaneData(image);
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data, image.getWidth(), image.getHeight(), 0, 0, image.getWidth(), image.getHeight(), false
);
QRCodeReader reader = new QRCodeReader();
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = reader.decode(bitmap);
if (_listener != null) {
new Handler(Looper.getMainLooper()).post(() -> _listener.onQrCodeDetected(result));
}
} catch (ChecksumException | FormatException | NotFoundException ignored) {
} finally {
image.close();
}
}
private static byte[] getLuminancePlaneData(ImageProxy image) {
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
ByteBuffer buf = plane.getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
int width = image.getWidth();
int height = image.getHeight();
int rowStride = plane.getRowStride();
int pixelStride = plane.getPixelStride();
if (width != rowStride || pixelStride != 1) {
// remove padding from the Y plane data
byte[] cleanData = new byte[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
cleanData[y * width + x] = data[y * rowStride + x * pixelStride];
}
}
return cleanData;
}
return data;
}
public interface Listener {
void onQrCodeDetected(Result result);
}
}
@@ -43,7 +43,7 @@ public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
int position = viewHolder.getAdapterPosition();
EntryAdapter adapter = (EntryAdapter)recyclerView.getAdapter();
if (adapter.getEntryAt(position) != _selectedEntry)
if (adapter.getEntryAt(position) != _selectedEntry || !isLongPressDragEnabled())
{
dragFlags = 0;
}
@@ -1,31 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import java.util.List;
import androidx.annotation.ArrayRes;
public class SpinnerHelper {
private SpinnerHelper() {
}
public static void fillSpinner(Context context, Spinner spinner, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, android.R.layout.simple_spinner_item);
initSpinner(spinner, adapter);
}
public static <T> void fillSpinner(Context context, Spinner spinner, List<T> items) {
ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item, items);
initSpinner(spinner, adapter);
}
private static void initSpinner(Spinner spinner, ArrayAdapter adapter) {
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.invalidate();
}
}
@@ -1,29 +0,0 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import me.dm7.barcodescanner.core.ViewFinderView;
public class SquareFinderView extends ViewFinderView {
public SquareFinderView(Context context) {
super(context);
init();
}
public SquareFinderView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setSquareViewFinder(true);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
@@ -48,6 +48,6 @@ public class TextDrawableHelper {
.width(view.getLayoutParams().width)
.height(view.getLayoutParams().height)
.endConfig()
.buildRect(text.substring(0, 1).toUpperCase(), color);
.buildRound(text.substring(0, 1).toUpperCase(), color);
}
}
@@ -0,0 +1,183 @@
package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Objects;
import com.google.common.io.Files;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPack {
private UUID _uuid;
private String _name;
private int _version;
private List<Icon> _icons;
private File _dir;
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
_uuid = uuid;
_name = name;
_version = version;
_icons = icons;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public int getVersion() {
return _version;
}
public List<Icon> getIcons() {
return Collections.unmodifiableList(_icons);
}
/**
* Retrieves a list of icons suggested for the given issuer.
*/
public List<Icon> getSuggestedIcons(String issuer) {
if (issuer == null || issuer.isEmpty()) {
return new ArrayList<>();
}
return _icons.stream()
.filter(i -> i.isSuggestedFor(issuer))
.collect(Collectors.toList());
}
@Nullable
public File getDirectory() {
return _dir;
}
void setDirectory(@NonNull File dir) {
_dir = dir;
}
/**
* Indicates whether some other object is "equal to" this one. The object does not
* necessarily have to be the same instance. Equality of UUID and version will make
* this method return true;
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof IconPack)) {
return false;
}
IconPack pack = (IconPack) o;
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
}
@Override
public int hashCode() {
return Objects.hashCode(_uuid, _version);
}
public static IconPack fromJson(JSONObject obj) throws JSONException {
UUID uuid;
String uuidString = obj.getString("uuid");
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
}
String name = obj.getString("name");
int version = obj.getInt("version");
JSONArray array = obj.getJSONArray("icons");
List<Icon> icons = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Icon icon = Icon.fromJson(array.getJSONObject(i));
icons.add(icon);
}
return new IconPack(uuid, name, version, icons);
}
public static IconPack fromBytes(byte[] data) throws JSONException {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return IconPack.fromJson(obj);
}
public static class Icon implements Serializable {
private final String _relFilename;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String category, List<String> issuers) {
_relFilename = filename;
_category = category;
_issuers = issuers;
}
public String getRelativeFilename() {
return _relFilename;
}
@Nullable
public File getFile() {
return _file;
}
void setFile(@NonNull File file) {
_file = file;
}
public IconType getIconType() {
return IconType.fromFilename(_relFilename);
}
@SuppressWarnings("UnstableApiUsage")
public String getName() {
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
public String getCategory() {
return _category;
}
public List<String> getIssuers() {
return Collections.unmodifiableList(_issuers);
}
public boolean isSuggestedFor(String issuer) {
return getIssuers().stream()
.anyMatch(is -> is.toLowerCase().contains(issuer.toLowerCase()));
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
List<String> issuers = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
String issuer = array.getString(i);
issuers.add(issuer);
}
return new Icon(filename, category, issuers);
}
}
}
@@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.icons;
public class IconPackException extends Exception {
public IconPackException(Throwable cause) {
super(cause);
}
public IconPackException(String message) {
super(message);
}
}
@@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.icons;
public class IconPackExistsException extends IconPackException {
private IconPack _pack;
public IconPackExistsException(IconPack pack) {
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
_pack = pack;
}
public IconPack getIconPack() {
return _pack;
}
}
@@ -0,0 +1,219 @@
package com.beemdevelopment.aegis.icons;
import android.content.Context;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.IOUtils;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPackManager {
private static final String _packDefFilename = "pack.json";
private File _iconsBaseDir;
private List<IconPack> _iconPacks;
public IconPackManager(Context context) {
_iconPacks = new ArrayList<>();
_iconsBaseDir = new File(context.getFilesDir(), "icons");
rescanIconPacks();
}
private IconPack getIconPackByUUID(UUID uuid) {
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
if (packs.size() == 0) {
return null;
}
return packs.get(0);
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
public void removeIconPack(IconPack pack) throws IconPackException {
try {
File dir = getIconPackDir(pack);
deleteDir(dir);
} catch (IOException e) {
throw new IconPackException(e);
}
_iconPacks.remove(pack);
}
public IconPack importPack(File inFile) throws IconPackException {
try {
// read and parse the icon pack definition file of the icon pack
ZipFile zipFile = new ZipFile(inFile);
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
if (packHeader == null) {
throw new IOException("Unable to find pack.json in the root of the ZIP file");
}
IconPack pack;
byte[] defBytes;
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
defBytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(defBytes);
}
// create a new directory to store the icon pack, based on the UUID and version
File packDir = getIconPackDir(pack);
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
throw new IOException("Attempted to write outside of the parent directory");
}
if (packDir.exists()) {
throw new IconPackExistsException(pack);
}
IconPack existingPack = getIconPackByUUID(pack.getUUID());
if (existingPack != null) {
throw new IconPackExistsException(existingPack);
}
if (!packDir.exists() && !packDir.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
// extract each of the defined icons to the icon pack directory
for (IconPack.Icon icon : pack.getIcons()) {
File destFile = new File(packDir, icon.getRelativeFilename());
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
if (iconHeader == null) {
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
}
// create new directories for this file if needed
File parent = destFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
FileOutputStream outStream = new FileOutputStream(destFile)) {
IOUtils.copy(inStream, outStream);
}
// after successful copy of the icon, store the new filename
icon.setFile(destFile);
}
// write the icon pack definition file to the newly created directory
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
outStream.write(defBytes);
}
// after successful extraction of the icon pack, store the new directory
pack.setDirectory(packDir);
_iconPacks.add(pack);
return pack;
} catch (IOException | JSONException e) {
throw new IconPackException(e);
}
}
private void rescanIconPacks() {
_iconPacks.clear();
File[] dirs = _iconsBaseDir.listFiles();
if (dirs == null) {
return;
}
for (File dir : dirs) {
if (!dir.isDirectory()) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(dir.getName());
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
}
File versionDir = getLatestVersionDir(dir);
if (versionDir != null) {
IconPack pack;
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
byte[] bytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(bytes);
pack.setDirectory(versionDir);
} catch (JSONException | IOException e) {
e.printStackTrace();
continue;
}
for (IconPack.Icon icon : pack.getIcons()) {
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
}
// do a sanity check on the UUID and version
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
_iconPacks.add(pack);
}
}
}
}
private File getIconPackDir(IconPack pack) {
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
}
@Nullable
private static File getLatestVersionDir(File packDir) {
File[] dirs = packDir.listFiles();
if (dirs == null) {
return null;
}
int latestVersion = -1;
for (File versionDir : dirs) {
int version;
try {
version = Integer.parseInt(versionDir.getName());
} catch (NumberFormatException ignored) {
continue;
}
if (latestVersion == -1 || version > latestVersion) {
latestVersion = version;
}
}
if (latestVersion == -1) {
return null;
}
return new File(packDir, Integer.toString(latestVersion));
}
private static void deleteDir(File dir) throws IOException {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDir(child);
}
}
}
if (!dir.delete()) {
throw new IOException(String.format("Unable to delete directory: %s", dir));
}
}
}
@@ -0,0 +1,52 @@
package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
public enum IconType {
INVALID,
SVG,
PNG,
JPEG;
public static IconType fromMimeType(String mimeType) {
switch (mimeType) {
case "image/svg+xml":
return SVG;
case "image/png":
return PNG;
case "image/jpeg":
return JPEG;
default:
return INVALID;
}
}
@SuppressWarnings("UnstableApiUsage")
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase()) {
case "svg":
return SVG;
case "png":
return PNG;
case "jpg":
// intentional fallthrough
case "jpeg":
return JPEG;
default:
return INVALID;
}
}
public String toMimeType() {
switch (this) {
case SVG:
return "image/svg+xml";
case PNG:
return "image/png";
case JPEG:
return "image/jpeg";
default:
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
}
}
}
@@ -1,20 +1,30 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.DialogInterface;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class AegisImporter extends DatabaseImporter {
@@ -23,19 +33,14 @@ public class AegisImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
byte[] bytes = IOUtils.readAll(stream);
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
@@ -58,14 +63,46 @@ public class AegisImporter extends DatabaseImporter {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws VaultFileException {
JSONObject obj = _file.getContent(creds);
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
JSONObject obj;
try {
obj = _file.getContent(creds);
} catch (VaultFileException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj);
}
public State decrypt(char[] password) throws DatabaseImporterException {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
return decrypt(creds);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, (Dialogs.TextInputListener) password -> {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
try {
if (result == null) {
throw new DatabaseImporterException("Password incorrect");
}
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
State state = decrypt(creds);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
}
}
@@ -102,7 +139,7 @@ public class AegisImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (JSONException | OtpInfoException | EncodingException e) {
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@@ -3,26 +3,32 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
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.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ProgressDialogTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
@@ -49,27 +55,20 @@ public class AndOtpImporter extends DatabaseImporter {
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
private static final int MAX_ITERATIONS = 10000;
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected String getAppPkgName() {
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = reader.readAll();
bytes = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
@@ -96,38 +95,12 @@ public class AndOtpImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password, boolean oldFormat) throws DatabaseImporterException {
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
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);
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);
@@ -137,13 +110,68 @@ public class AndOtpImporter extends DatabaseImporter {
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| InvalidKeySpecException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private KeyDerivationParams getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new KeyDerivationParams(password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
// 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;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
return decryptContent(key, 0);
}
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
return decryptContent(key, INT_SIZE + SALT_SIZE);
}
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
KeyDerivationParams params = getKeyDerivationParams(password);
SecretKey key = AndOtpKeyDerivationTask.deriveKey(params);
return decryptNewFormat(key);
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
KeyDerivationParams params = getKeyDerivationParams(password);
AndOtpKeyDerivationTask task = new AndOtpKeyDerivationTask(context, key -> {
try {
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
@@ -158,12 +186,11 @@ public class AndOtpImporter extends DatabaseImporter {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
DecryptedState state = decrypt(password, i != 0);
listener.onStateDecrypted(state);
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
})
.create());
}
@@ -212,7 +239,7 @@ public class AndOtpImporter extends DatabaseImporter {
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", 30));
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
@@ -221,12 +248,17 @@ public class AndOtpImporter extends DatabaseImporter {
String name;
String issuer = "";
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
name = parts[0];
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
@@ -235,4 +267,66 @@ public class AndOtpImporter extends DatabaseImporter {
}
}
}
protected static class AndOtpKeyDerivationTask extends ProgressDialogTask<AndOtpImporter.KeyDerivationParams, SecretKey> {
private Callback _cb;
public AndOtpKeyDerivationTask(Context context, Callback cb) {
super(context, context.getString(R.string.unlocking_vault));
_cb = cb;
}
@Override
protected SecretKey doInBackground(AndOtpImporter.KeyDerivationParams... args) {
setPriority();
AndOtpImporter.KeyDerivationParams params = args[0];
return deriveKey(params);
}
protected static SecretKey deriveKey(KeyDerivationParams params) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(params.getPassword(), params.getSalt(), params.getIterations(), KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
}
@Override
protected void onPostExecute(SecretKey key) {
super.onPostExecute(key);
_cb.onTaskFinished(key);
}
public interface Callback {
void onTaskFinished(SecretKey key);
}
}
protected static class KeyDerivationParams {
private final char[] _password;
private final byte[] _salt;
private final int _iterations;
public KeyDerivationParams(char[] password, byte[] salt, int iterations) {
_iterations = iterations;
_password = password;
_salt = salt;
}
public char[] getPassword() {
return _password;
}
public int getIterations() {
return _iterations;
}
public byte[] getSalt() {
return _salt;
}
}
}
@@ -2,7 +2,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
@@ -11,6 +13,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
@@ -20,54 +23,55 @@ public class AuthenticatorPlusImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(reader.readAll());
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
private final byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected State decrypt(char[] password) throws DatabaseImporterException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
return importer.read(zipStream);
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
FileReader reader = new FileReader(zipStream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(context);
GoogleAuthUriImporter.State state = importer.read(reader);
listener.onStateDecrypted(state);
return;
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException | DatabaseImporterException e) {
try {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
}
}
}
@@ -1,18 +1,23 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.json.JSONArray;
import org.json.JSONException;
@@ -21,6 +26,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -36,10 +42,13 @@ import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/com.authy.storage.tokens.authenticator.xml";
private static final String _subPath = "shared_prefs";
private static final String _pkgName = "com.authy.authy";
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
private static final String _authyFilename = "com.authy.storage.tokens.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
@@ -53,42 +62,91 @@ public class AuthyImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
JSONArray array;
JSONArray authyArray;
try {
array = readFile(new SuFile(path, String.format("%s.xml", _authFilename)), String.format("%s.key", _authFilename));
authyArray = readFile(new SuFile(path, String.format("%s.xml", _authyFilename)), String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
try {
for (int i = 0; i < authyArray.length(); i++) {
array.put(authyArray.getJSONObject(i));
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return read(array);
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.setInput(stream, null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals("com.authy.storage.tokens.authenticator.key")) {
if (entry.Name.equals(String.format("%s.key", _authFilename))
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
array = new JSONArray(entry.Value);
break;
}
}
for (int i = 0; i < array.length(); i++) {
if (!array.getJSONObject(i).has("decryptedSecret")) {
return new EncryptedState(array);
}
}
return new DecryptedState(array);
return read(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
private State read(JSONArray array) throws DatabaseImporterException {
try {
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
return new EncryptedState(array);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(array);
}
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(inStream, null);
parser.nextTag();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(key)) {
return new JSONArray(entry.Value);
}
}
} catch (JSONException ignored) {
}
return new JSONArray();
}
public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _array;
@@ -97,47 +155,56 @@ public class AuthyImporter extends DatabaseImporter {
_array = array;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
key = new SecretKeySpec(key.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
return new DecryptedState(_array);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = obj.optString("encryptedSecret", null);
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
DecryptedState state = new DecryptedState(_array);
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
}, dialog1 -> listener.onCanceled());
}
}
@@ -173,40 +240,54 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.optString("originalName", null);
authyEntryInfo.OriginalIssuer = entry.optString("originalIssuer", null);
authyEntryInfo.AccountType = entry.getString("accountType");
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
sanitizeEntryInfo(authyEntryInfo);
boolean isAuthy = entry.has("secretSeed");
sanitizeEntryInfo(authyEntryInfo, isAuthy);
byte[] secret;
if (isAuthy) {
secret = Hex.decode(entry.getString("secretSeed"));
} else {
secret = Base32.decode(entry.getString("decryptedSecret"));
}
int digits = entry.getInt("digits");
byte[] secret = Base32.decode(entry.getString("decryptedSecret"));
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String separator = "";
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
if (!isAuthy) {
String separator = "";
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + separator, "");
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
info.Issuer = info.Name;
info.Name = "";
}
info.Name = info.Name.replace(info.Issuer + separator, "");
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}
@@ -3,50 +3,43 @@ 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.IOUtils;
import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
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 java.util.stream.Collectors;
public abstract class DatabaseImporter {
private Context _context;
private static Map<String, Class<? extends DatabaseImporter>> _importers;
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
private static List<Definition> _importers;
static {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.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("Microsoft Authenticator", MicrosoftAuthImporter.class);
_importers.put("Plain text", GoogleAuthUriImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.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("Microsoft Authenticator", MicrosoftAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers = new ArrayList<>();
_importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false));
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
public DatabaseImporter(Context context) {
@@ -57,20 +50,27 @@ public abstract class DatabaseImporter {
return _context;
}
public SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(getAppPkgName(), getAppSubPath());
}
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = getContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
protected abstract String getAppPkgName();
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
protected abstract String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
public State read(InputStream stream) throws DatabaseImporterException {
return read(stream, false);
}
public abstract State read(FileReader reader) throws DatabaseImporterException;
public State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile file = getAppPath();
try (SuFileInputStream stream = new SuFileInputStream(file)) {
return read(stream, true);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
try {
@@ -81,12 +81,42 @@ public abstract class DatabaseImporter {
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return Collections.unmodifiableMap(_importers);
public static List<Definition> getImporters(boolean isDirect) {
if (isDirect) {
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
}
return Collections.unmodifiableList(_importers);
}
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
return Collections.unmodifiableMap(_appImporters);
public static class Definition {
private final String _name;
private final Class<? extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
_help = help;
_supportsDirect = supportsDirect;
}
public String getName() {
return _name;
}
public Class<? extends DatabaseImporter> getType() {
return _type;
}
public @StringRes int getHelp() {
return _help;
}
public boolean supportsDirect() {
return _supportsDirect;
}
}
public static abstract class State {
@@ -138,38 +168,9 @@ public abstract class DatabaseImporter {
}
}
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 {
return IOUtils.readAll(_stream);
}
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);
public static abstract class DecryptListener {
protected abstract void onStateDecrypted(State state);
protected abstract void onError(Exception e);
protected abstract void onCanceled();
}
}
@@ -1,15 +1,17 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
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 com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
@@ -18,6 +20,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@@ -30,21 +33,16 @@ public class FreeOtpImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.setInput(stream, null);
parser.nextTag();
List<JSONObject> entries = new ArrayList<>();
@@ -117,30 +115,6 @@ public class FreeOtpImporter extends DatabaseImporter {
}
}
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) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
JSONObject entry = parseXmlEntry(parser);
if (entry != null) {
entries.add(entry);
}
}
return entries;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
@@ -148,46 +122,4 @@ public class FreeOtpImporter extends DatabaseImporter {
}
return bytes;
}
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 = parseXmlText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
if (name.equals("tokenOrder")) {
return null;
}
return new JSONObject(value);
}
private static String parseXmlText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}
@@ -1,12 +1,17 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -20,24 +25,19 @@ public class FreeOtpPlusImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
State state;
if (reader.isInternal()) {
state = new FreeOtpImporter(getContext()).read(reader);
if (isInternal) {
state = new FreeOtpImporter(getContext()).read(stream);
} else {
try {
String json = new String(reader.readAll(), StandardCharsets.UTF_8);
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
@@ -1,16 +1,19 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
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.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class GoogleAuthImporter extends DatabaseImporter {
@@ -25,19 +28,22 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
@@ -67,7 +73,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret());
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
@@ -5,9 +5,11 @@ import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
@@ -17,20 +19,15 @@ public class GoogleAuthUriImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return null;
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public GoogleAuthUriImporter.State read(DatabaseImporter.FileReader reader) throws DatabaseImporterException {
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(reader.getStream());
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base32;
@@ -10,7 +11,9 @@ import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
@@ -25,19 +28,22 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, reader.getStream(), "accounts");
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp() throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
@@ -85,7 +91,7 @@ public class MicrosoftAuthImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
OtpInfo info = new TotpInfo(secret, "SHA1", digits, 30);
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
@@ -1,11 +1,15 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.topjohnwu.superuser.ShellUtils;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.common.io.Files;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
@@ -24,19 +28,75 @@ public class SqlImporterHelper {
_context = context;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file;
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
File dir = Files.createTempDir();
File mainFile = new File(dir, path.getName());
List<File> fileCopies = new ArrayList<>();
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
// create temporary copies of the database files so that SQLiteDatabase can open them
File fileCopy = null;
try (SuFileInputStream inStream = new SuFileInputStream(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
}
fileCopies.add(fileCopy);
} catch (IOException e) {
if (fileCopy != null) {
fileCopy.delete();
}
for (File fileCopy2 : fileCopies) {
fileCopy2.delete();
}
throw new DatabaseImporterException(e);
}
}
try {
return read(type, mainFile, table);
} finally {
for (File fileCopy : fileCopies) {
fileCopy.delete();
}
}
}
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
}
return files;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file = null;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("db-import-", "", _context.getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(inStream, out);
IOUtils.copy(inStream, out);
}
} catch (IOException e) {
if (file != null) {
file.delete();
}
throw new DatabaseImporterException(e);
}
try {
return read(type, file, table);
} finally {
// always delete the temporary file
file.delete();
}
}
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();
@@ -55,16 +115,15 @@ public class SqlImporterHelper {
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
@@ -73,10 +132,12 @@ public class SqlImporterHelper {
return res;
}
@SuppressLint("Range")
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
@@ -7,6 +7,7 @@ import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
@@ -14,6 +15,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class SteamImporter extends DatabaseImporter {
@@ -25,27 +27,22 @@ public class SteamImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
}
@Override
protected String getAppSubPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
protected SuFile getAppPath() 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 path = getAppPath(_pkgName, _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();
return files[0];
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = reader.readAll();
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
@@ -13,9 +14,11 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
@@ -24,6 +27,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -59,22 +63,17 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (reader.isInternal()) {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(reader.getStream(), null);
parser.setInput(stream, null);
parser.nextTag();
String data = null;
@@ -91,7 +90,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = reader.readAll();
byte[] base64 = IOUtils.readAll(stream);
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
@@ -105,8 +104,8 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
String s = array.getString(i);
entries.add(new JSONObject(s));
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
@@ -120,7 +119,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
_data = data;
}
private DecryptedState decrypt(char[] password) throws DatabaseImporterException {
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
@@ -128,7 +127,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
@@ -160,7 +159,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
});
}, dialog1 -> listener.onCanceled());
})
.setNegativeButton(R.string.no, (dialog, which) -> {
decrypt(PASSWORD, listener);
@@ -222,7 +221,7 @@ public class TotpAuthenticatorImporter extends DatabaseImporter {
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret, "SHA1", 6, 30);
TotpInfo info = new TotpInfo(secret);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
@@ -0,0 +1,94 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class TwoFASImporter extends DatabaseImporter {
public TwoFASImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 1) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
JSONArray array = obj.getJSONArray("services");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
return new TwoFASImporter.State(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final 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 {
byte[] secret = Base32.decode(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = info.getString("issuer");
String name = info.optString("account");
OtpInfo otp = new TotpInfo(secret);
return new VaultEntry(otp, name, issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
public class WinAuthImporter extends DatabaseImporter {
public WinAuthImporter(Context context) {
@@ -10,32 +13,27 @@ public class WinAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return null;
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected String getAppSubPath() {
return null;
}
@Override
public WinAuthImporter.State read(FileReader reader) throws DatabaseImporterException {
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
GoogleAuthUriImporter.State state = importer.read(reader);
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private GoogleAuthUriImporter.State _state;
private DatabaseImporter.State _state;
private State(GoogleAuthUriImporter.State state) {
private State(DatabaseImporter.State state) {
super(false);
_state = state;
}
@Override
public Result convert() {
public Result convert() throws DatabaseImporterException {
Result result = _state.convert();
for (VaultEntry entry : result.getEntries()) {
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.ui.glide;
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
@@ -0,0 +1,34 @@
package com.beemdevelopment.aegis.licenses;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import de.psdev.licensesdialog.licenses.License;
public class ProtobufLicense extends License {
@Override
public String getName() {
return "Protocol Buffers License";
}
@Override
public String readSummaryTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String readFullTextFromResources(Context context) {
return getContent(context, R.raw.protobuf_license);
}
@Override
public String getVersion() {
return null;
}
@Override
public String getUrl() {
return "https://raw.githubusercontent.com/protocolbuffers/protobuf/master/LICENSE";
}
}
@@ -8,10 +8,11 @@ import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class GoogleAuthInfo {
public class GoogleAuthInfo implements Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
@@ -49,7 +50,7 @@ public class GoogleAuthInfo {
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
builder.appendQueryParameter("secret", new String(Base32.encode(_info.getSecret())));
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
if (_issuer != null && !_issuer.equals("")) {
builder.path(String.format("%s:%s", _issuer, _accountName));
@@ -64,7 +65,7 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException(String.format("Bad URI format: %s", s));
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
@@ -72,29 +73,27 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
}
// 'secret' is a required parameter
String encodedSecret = uri.getQueryParameter("secret");
if (encodedSecret == null) {
throw new GoogleAuthInfoException("Parameter 'secret' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
}
// decode secret
byte[] secret;
try {
secret = Base32.decode(encodedSecret);
secret = parseSecret(encodedSecret);
} catch (EncodingException e) {
throw new GoogleAuthInfoException("Bad secret", e);
throw new GoogleAuthInfoException(uri, "Bad secret", e);
}
// check the otp type
OtpInfo info;
try {
String type = uri.getHost();
if (type == null) {
throw new GoogleAuthInfoException(String.format("Host not present in URI: %s", uri.toString()));
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
@@ -118,16 +117,16 @@ public class GoogleAuthInfo {
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("Parameter 'counter' is not present");
throw new GoogleAuthInfoException(uri, "Parameter 'counter' is not present");
}
hotpInfo.setCounter(Long.parseLong(counter));
info = hotpInfo;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported OTP type: %s", type));
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
// provider info used to disambiguate accounts
@@ -167,16 +166,24 @@ public class GoogleAuthInfo {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
return new GoogleAuthInfo(info, accountName, issuer);
}
/**
* Decodes the given base 32 secret, while being tolerant of whitespace and dashes.
*/
public static byte[] parseSecret(String s) throws EncodingException {
s = s.trim().replace("-", "").replace(" ", "");
return Base32.decode(s);
}
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("Bad URI format");
throw new GoogleAuthInfoException(uri, "Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
@@ -184,17 +191,17 @@ public class GoogleAuthInfo {
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException("Unsupported protocol");
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException("Unsupported host");
throw new GoogleAuthInfoException(uri, "Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException("Parameter 'data' is not set");
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
@@ -202,26 +209,59 @@ public class GoogleAuthInfo {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
List<GoogleAuthInfo> infos = new ArrayList<>();
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_HOTP:
otp = new HotpInfo(secret, params.getCounter());
int digits;
switch (params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = TotpInfo.DEFAULT_DIGITS;
break;
case OTP_TOTP:
otp = new TotpInfo(secret);
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
default:
throw new GoogleAuthInfoException(String.format("Unsupported algorithm: %d", params.getType().ordinal()));
throw new GoogleAuthInfoException(uri, String.format("Unsupported digits: %d", params.getDigits().ordinal()));
}
String algo;
switch (params.getAlgorithm()) {
case ALGORITHM_UNSPECIFIED:
// intentional fallthrough
case ALGORITHM_SHA1:
algo = "SHA1";
break;
case ALGORITHM_SHA256:
algo = "SHA256";
break;
case ALGORITHM_SHA512:
algo = "SHA512";
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported hash algorithm: %d", params.getAlgorithm().ordinal()));
}
byte[] secret = params.getSecret().toByteArray();
switch (params.getType()) {
case OTP_TYPE_UNSPECIFIED:
// intentional fallthrough
case OTP_TYPE_TOTP:
otp = new TotpInfo(secret, algo, digits, TotpInfo.DEFAULT_PERIOD);
break;
case OTP_TYPE_HOTP:
otp = new HotpInfo(secret, algo, digits, params.getCounter());
break;
default:
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
}
} catch (OtpInfoException e){
throw new GoogleAuthInfoException(e);
throw new GoogleAuthInfoException(uri, e);
}
String name = params.getName();
@@ -1,16 +1,30 @@
package com.beemdevelopment.aegis.otp;
import android.net.Uri;
public class GoogleAuthInfoException extends Exception {
public GoogleAuthInfoException(Throwable cause) {
private final Uri _uri;
public GoogleAuthInfoException(Uri uri, Throwable cause) {
super(cause);
_uri = uri;
}
public GoogleAuthInfoException(String message) {
public GoogleAuthInfoException(Uri uri, String message) {
super(message);
_uri = uri;
}
public GoogleAuthInfoException(String message, Throwable cause) {
public GoogleAuthInfoException(Uri uri, String message, Throwable cause) {
super(message, cause);
_uri = uri;
}
/**
* Reports whether the scheme of the URI is phonefactor://.
*/
public boolean isPhoneFactor() {
return _uri != null && _uri.getScheme() != null && _uri.getScheme().equals("phonefactor");
}
@Override
@@ -19,6 +33,7 @@ public class GoogleAuthInfoException extends Exception {
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}
@@ -11,6 +11,7 @@ import java.security.NoSuchAlgorithmException;
public class HotpInfo extends OtpInfo {
public static final String ID = "hotp";
public static final int DEFAULT_COUNTER = 0;
private long _counter;
@@ -20,7 +21,7 @@ public class HotpInfo extends OtpInfo {
}
public HotpInfo(byte[] secret) throws OtpInfoException {
this(secret, 0);
this(secret, DEFAULT_COUNTER);
}
public HotpInfo(byte[] secret, String algorithm, int digits, long counter) throws OtpInfoException {
@@ -39,7 +40,7 @@ public class HotpInfo extends OtpInfo {
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@@ -10,12 +10,15 @@ import java.io.Serializable;
import java.util.Arrays;
public abstract class OtpInfo implements Serializable {
public static final int DEFAULT_DIGITS = 6;
public static final String DEFAULT_ALGORITHM = "SHA1";
private byte[] _secret;
private String _algorithm;
private int _digits;
public OtpInfo(byte[] secret) throws OtpInfoException {
this(secret, "SHA1", 6);
this(secret, DEFAULT_ALGORITHM, DEFAULT_DIGITS);
}
public OtpInfo(byte[] secret, String algorithm, int digits) throws OtpInfoException {
@@ -26,7 +29,11 @@ public abstract class OtpInfo implements Serializable {
public abstract String getOtp();
public abstract String getType();
public abstract String getTypeId();
public String getType() {
return getTypeId().toUpperCase();
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
@@ -127,7 +134,7 @@ public abstract class OtpInfo implements Serializable {
}
OtpInfo info = (OtpInfo) o;
return getType().equals(info.getType())
return getTypeId().equals(info.getTypeId())
&& Arrays.equals(getSecret(), info.getSecret())
&& getAlgorithm(false).equals(info.getAlgorithm(false))
&& getDigits() == info.getDigits();
@@ -8,9 +8,10 @@ import java.security.NoSuchAlgorithmException;
public class SteamInfo extends TotpInfo {
public static final String ID = "steam";
public static final int DIGITS = 5;
public SteamInfo(byte[] secret) throws OtpInfoException {
super(secret, "SHA1", 5, 30);
super(secret, OtpInfo.DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
}
public SteamInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
@@ -28,7 +29,13 @@ public class SteamInfo extends TotpInfo {
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@Override
public String getType() {
String id = getTypeId();
return id.substring(0, 1).toUpperCase() + id.substring(1);
}
}
@@ -11,12 +11,13 @@ import java.security.NoSuchAlgorithmException;
public class TotpInfo extends OtpInfo {
public static final String ID = "totp";
public static final int DEFAULT_PERIOD = 30;
private int _period;
public TotpInfo(byte[] secret) throws OtpInfoException {
super(secret);
setPeriod(30);
setPeriod(DEFAULT_PERIOD);
}
public TotpInfo(byte[] secret, String algorithm, int digits, int period) throws OtpInfoException {
@@ -34,8 +35,17 @@ public class TotpInfo extends OtpInfo {
}
}
public String getOtp(long time) {
try {
OTP otp = TOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getPeriod(), time);
return otp.toString();
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@@ -6,13 +6,12 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.StringRes;
import androidx.core.view.LayoutInflaterCompat;
@@ -20,7 +19,10 @@ 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.beemdevelopment.aegis.licenses.GlideLicense;
import com.beemdevelopment.aegis.licenses.ProtobufLicense;
import com.beemdevelopment.aegis.ui.dialogs.ChangelogDialog;
import com.beemdevelopment.aegis.ui.dialogs.LicenseDialog;
import com.mikepenz.iconics.context.IconicsLayoutInflater2;
import de.psdev.licensesdialog.LicenseResolver;
@@ -42,9 +44,22 @@ public class AboutActivity extends AegisActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
setSupportActionBar(findViewById(R.id.toolbar));
View btnLicenses = findViewById(R.id.btn_licenses);
btnLicenses.setOnClickListener(v -> showLicenseDialog());
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
View btnLicense = findViewById(R.id.btn_license);
btnLicense.setOnClickListener(v -> {
LicenseDialog.create()
.setTheme(getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
View btnThirdPartyLicenses = findViewById(R.id.btn_third_party_licenses);
btnThirdPartyLicenses.setOnClickListener(v -> showThirdPartyLicenseDialog());
TextView appVersion = findViewById(R.id.app_version);
appVersion.setText(getCurrentAppVersion());
@@ -74,7 +89,9 @@ public class AboutActivity extends AegisActivity {
View btnChangelog = findViewById(R.id.btn_changelog);
btnChangelog.setOnClickListener(v -> {
ChangelogDialog.create().setTheme(getCurrentTheme()).show(getSupportFragmentManager(), "CHANGELOG_DIALOG");
ChangelogDialog.create()
.setTheme(getConfiguredTheme())
.show(getSupportFragmentManager(), null);
});
}
@@ -110,9 +127,9 @@ public class AboutActivity extends AegisActivity {
startActivity(Intent.createChooser(mailIntent, this.getString(R.string.email)));
}
private void showLicenseDialog() {
private void showThirdPartyLicenseDialog() {
String stylesheet = getString(R.string.custom_notices_format_style);
int backgroundColorResource = getCurrentTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
int backgroundColorResource = getConfiguredTheme() == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
String backgroundColor = getThemeColorAsHex(backgroundColorResource);
String textColor = getThemeColorAsHex(R.attr.primaryText);
String licenseColor = getThemeColorAsHex(R.attr.cardBackgroundFocused);
@@ -121,9 +138,11 @@ public class AboutActivity extends AegisActivity {
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor, linkColor);
LicenseResolver.registerLicense(new GlideLicense());
LicenseResolver.registerLicense(new ProtobufLicense());
new LicensesDialog.Builder(this)
.setNotices(R.raw.notices)
.setTitle(R.string.licenses)
.setTitle(R.string.third_party_licenses)
.setNoticesCssStyle(stylesheet)
.setIncludeOwnLicense(true)
.build()
@@ -133,4 +152,17 @@ public class AboutActivity extends AegisActivity {
private String getThemeColorAsHex(@AttrRes int attributeId) {
return String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(attributeId, getTheme())));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,28 +1,33 @@
package com.beemdevelopment.aegis.ui;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.provider.Settings;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import java.util.Map;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _currentTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -30,15 +35,16 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
// set the theme and locale before creating the activity
Preferences prefs = getPreferences();
setPreferredTheme(prefs.getCurrentTheme());
onSetTheme();
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);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
@@ -47,37 +53,34 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
// 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
@CallSuper
protected void onDestroy() {
_app.unregisterLockListener(this);
super.onDestroy();
}
@CallSuper
@Override
protected void onResume() {
super.onResume();
_resumed = true;
_app.setBlockAutoLock(false);
}
@Override
protected void onPause() {
super.onPause();
_resumed = false;
}
@CallSuper
@Override
public void onLocked() {
if (isOrphan()) {
setResult(RESULT_CANCELED, null);
finish();
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
try {
Method method = Activity.class.getDeclaredMethod("finish", int.class);
method.setAccessible(true);
method.invoke(this, 2); // FINISH_TASK_WITH_ACTIVITY = 2
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
finishAndRemoveTask();
}
}
@@ -89,33 +92,35 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
return _app.getPreferences();
}
protected void setPreferredTheme(Theme theme) {
/**
* Called when the activity is expected to set its theme.
*/
protected void onSetTheme() {
setTheme(ThemeMap.DEFAULT);
}
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
protected void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
setTheme(theme);
}
protected Theme getConfiguredTheme() {
Theme theme = getPreferences().getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
// set the theme based on the system theme
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
case Configuration.UI_MODE_NIGHT_NO:
theme = Theme.LIGHT;
break;
case Configuration.UI_MODE_NIGHT_YES:
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
break;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
_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;
}
return theme;
}
protected void setLocale(Locale locale) {
@@ -127,9 +132,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
}
protected boolean saveVault() {
protected boolean saveVault(boolean backup) {
try {
getApp().getVaultManager().save();
getApp().getVaultManager().save(backup);
return true;
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
@@ -137,33 +142,74 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
/**
* 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.isVaultLocked();
protected boolean isOrphan() {
return !(this instanceof MainActivity) && !(this instanceof AuthActivity) && !(this instanceof IntroActivity) && _app.isVaultLocked();
}
private void setGlobalAnimationDurationScale() {
float durationScale = Settings.Global.getFloat(getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 0);
if (durationScale != 1) {
public static class Helper {
private Helper() {
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivityForResult(Activity activity, Intent intent, int requestCode) {
AegisApplication app = (AegisApplication) activity.getApplication();
app.setBlockAutoLock(true);
try {
ValueAnimator.class.getMethod("setDurationScale", float.class).invoke(null, 1f);
} catch (Throwable t) {
Toast.makeText(this, R.string.progressbar_error, Toast.LENGTH_SHORT).show();
activity.startActivityForResult(intent, requestCode, null);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(activity, R.string.documentsui_error, e);
} else {
throw e;
}
}
}
}
protected Theme getCurrentTheme() {
return _currentTheme;
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivity(Fragment fragment, Intent intent) {
startExtActivityForResult(fragment, intent, -1);
}
/**
* Starts an external activity, temporarily blocks automatic lock of Aegis and
* shows an error dialog if the target activity is not found.
*/
public static void startExtActivityForResult(Fragment fragment, Intent intent, int requestCode) {
AegisApplication app = (AegisApplication) fragment.getActivity().getApplication();
app.setBlockAutoLock(true);
try {
fragment.startActivityForResult(intent, requestCode, null);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
if (isDocsAction(intent.getAction())) {
Dialogs.showErrorDialog(fragment.getContext(), R.string.documentsui_error, e);
} else {
throw e;
}
}
}
private static boolean isDocsAction(@Nullable String action) {
return action != null && (action.equals(Intent.ACTION_GET_CONTENT)
|| action.equals(Intent.ACTION_CREATE_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT)
|| action.equals(Intent.ACTION_OPEN_DOCUMENT_TREE));
}
}
}
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -14,6 +15,7 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -21,19 +23,20 @@ import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.crypto.MasterKey;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.MetricsHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
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;
@@ -50,15 +53,18 @@ import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
private int _failedUnlockAttempts;
// the first time this activity is resumed after creation, it's possible to inhibit showing the
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
private boolean _inhibitBioPrompt;
private Preferences _prefs;
private boolean _stateless;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -68,7 +74,7 @@ public class AuthActivity extends AegisActivity {
_textPassword = findViewById(R.id.text_password);
LinearLayout boxBiometricInfo = findViewById(R.id.box_biometric_info);
Button decryptButton = findViewById(R.id.button_decrypt);
Button biometricsButton = findViewById(R.id.button_biometrics);
TextView biometricsButton = findViewById(R.id.button_biometrics);
_textPassword.setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
@@ -77,21 +83,24 @@ public class AuthActivity extends AegisActivity {
return false;
});
Intent intent = getIntent();
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
_slots = (SlotList) intent.getSerializableExtra("slots");
_stateless = _slots != null;
if (!_stateless) {
VaultFile vaultFile;
try {
vaultFile = getApp().loadVaultFile();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
return;
}
if (_prefs.isPinKeyboardEnabled()) {
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
Intent intent = getIntent();
if (savedInstanceState == null) {
_inhibitBioPrompt = intent.getBooleanExtra("inhibitBioPrompt", false);
} else {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
try {
VaultFile vaultFile = getApp().loadVaultFile();
_slots = vaultFile.getHeader().getSlots();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog, which) -> onBackPressed());
return;
}
// 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
@@ -126,6 +135,7 @@ public class AuthActivity extends AegisActivity {
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxBiometricInfo.setVisibility(View.VISIBLE);
biometricsButton.setVisibility(View.GONE);
}
}
@@ -136,7 +146,8 @@ public class AuthActivity extends AegisActivity {
char[] password = EditTextHelper.getEditTextChars(_textPassword);
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener()).execute(params);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
});
biometricsButton.setOnClickListener(v -> {
@@ -144,6 +155,17 @@ public class AuthActivity extends AegisActivity {
});
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
private void selectPassword() {
_textPassword.selectAll();
@@ -153,32 +175,38 @@ public class AuthActivity extends AegisActivity {
@Override
public void onBackPressed() {
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
finish();
}
finishAffinity();
}
@Override
public void onResume() {
super.onResume();
if (_bioKey != null) {
if (_prefs.isPasswordReminderNeeded()) {
focusPasswordField();
} else {
showBiometricPrompt();
}
} else {
boolean remindPassword = _prefs.isPasswordReminderNeeded();
if (_bioKey == null || remindPassword) {
focusPasswordField();
}
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
_bioPrompt = showBiometricPrompt();
}
_inhibitBioPrompt = false;
}
@Override
public void onPause() {
if (!isChangingConfigurations() && _bioPrompt != null) {
_bioPrompt.cancelAuthentication();
_bioPrompt = null;
}
super.onPause();
}
@Override
public void onAttachedToWindow() {
if (_bioPrompt != null && _prefs.isPasswordReminderNeeded()) {
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
showPasswordReminder();
}
}
@@ -190,74 +218,84 @@ public class AuthActivity extends AegisActivity {
private void showPasswordReminder() {
View popupLayout = getLayoutInflater().inflate(R.layout.popup_password, null);
popupLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
PopupWindow popup = new PopupWindow(popupLayout, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setFocusable(false);
popup.setOutsideTouchable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
popup.setElevation(5.0f);
}
_textPassword.post(() -> popup.showAsDropDown(_textPassword));
_textPassword.post(() -> {
// calculating the actual height of the popup window does not seem possible
// adding 25dp seems to look good enough
int yoff = _textPassword.getHeight()
+ popupLayout.getMeasuredHeight()
+ MetricsHelper.convertDpToPixels(this, 25);
popup.showAsDropDown(_textPassword, 0, -yoff);
});
_textPassword.postDelayed(popup::dismiss, 5000);
}
public void showBiometricPrompt() {
public BiometricPrompt showBiometricPrompt() {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(_textPassword.getWindowToken(), 0);
Cipher cipher;
try {
cipher = _bioSlot.createDecryptCipher(_bioKey);
} catch (SlotException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
return;
return null;
}
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt prompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(false)
.build();
_bioPrompt.authenticate(info, cryptoObj);
}
@Override
public void onPause() {
super.onPause();
if (_bioPrompt != null) {
_bioPrompt.cancelAuthentication();
}
prompt.authenticate(info, cryptoObj);
return prompt;
}
private void finish(MasterKey key, boolean isSlotRepaired) {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
if (_stateless) {
// send the master key back to the calling activity
Intent intent = new Intent();
intent.putExtra("creds", creds);
setResult(RESULT_OK, intent);
} else {
try {
AegisApplication app = getApp();
VaultManager vault = app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
vault.setCredentials(creds);
saveVault();
}
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
try {
AegisApplication app = getApp();
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
saveVault(true);
}
setResult(RESULT_OK);
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.decryption_corrupt_error, e);
return;
}
setResult(RESULT_OK);
finish();
}
private void onInvalidPassword() {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
_failedUnlockAttempts ++;
if (_failedUnlockAttempts >= 3) {
_textPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
}
private class PasswordDerivationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
@@ -273,12 +311,7 @@ public class AuthActivity extends AegisActivity {
finish(result.getKey(), result.isSlotRepaired());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(AuthActivity.this)
.setTitle(getString(R.string.unlock_vault_error))
.setMessage(getString(R.string.unlock_vault_error_description))
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> selectPassword())
.create());
onInvalidPassword();
}
}
}
@@ -287,6 +320,8 @@ public class AuthActivity extends AegisActivity {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
_bioPrompt = null;
if (!BiometricsHelper.isCanceled(errorCode)) {
Toast.makeText(AuthActivity.this, errString, Toast.LENGTH_LONG).show();
}
@@ -295,6 +330,7 @@ public class AuthActivity extends AegisActivity {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_bioPrompt = null;
MasterKey key;
BiometricSlot slot = _slots.find(BiometricSlot.class);
@@ -1,94 +0,0 @@
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;
}
}
@@ -1,60 +1,80 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.TableRow;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.documentfile.provider.DocumentFile;
import com.amulyakhare.textdrawable.TextDrawable;
import com.avito.android.krop.KropView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.SpinnerHelper;
import com.beemdevelopment.aegis.helpers.IconViewHelper;
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.util.Cloner;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
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.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -62,28 +82,30 @@ public class EditEntryActivity extends AegisActivity {
private static final int PICK_IMAGE_REQUEST = 0;
private boolean _isNew = false;
private boolean _isManual = false;
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
private boolean _hasChangedIcon = false;
private IconPack.Icon _selectedIcon;
private boolean _isEditingIcon;
private CircleImageView _iconView;
private ImageView _saveImageButton;
private EditText _textName;
private EditText _textIssuer;
private EditText _textPeriod;
private EditText _textCounter;
private EditText _textDigits;
private EditText _textSecret;
private TextInputEditText _textName;
private TextInputEditText _textIssuer;
private TextInputEditText _textPeriodCounter;
private TextInputLayout _textPeriodCounterLayout;
private TextInputEditText _textDigits;
private TextInputLayout _textDigitsLayout;
private TextInputEditText _textSecret;
private TableRow _rowPeriod;
private TableRow _rowCounter;
private Spinner _spinnerType;
private Spinner _spinnerAlgo;
private Spinner _spinnerGroup;
private List<String> _spinnerGroupList = new ArrayList<>();
private AutoCompleteTextView _dropdownType;
private AutoCompleteTextView _dropdownAlgo;
private TextInputLayout _dropdownAlgoLayout;
private AutoCompleteTextView _dropdownGroup;
private List<String> _dropdownGroupList = new ArrayList<>();
private KropView _kropView;
@@ -96,6 +118,7 @@ public class EditEntryActivity extends AegisActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit_entry);
setSupportActionBar(findViewById(R.id.toolbar));
_vault = getApp().getVaultManager();
_groups = _vault.getGroups();
@@ -111,40 +134,59 @@ public class EditEntryActivity extends AegisActivity {
_origEntry = _vault.getEntryByUUID(entryUUID);
} else {
_origEntry = (VaultEntry) intent.getSerializableExtra("newEntry");
_isManual = intent.getBooleanExtra("isManual", false);
_isNew = true;
setTitle(R.string.add_new_entry);
}
String selectedGroup = intent.getStringExtra("selectedGroup");
// set up fields
_iconView = findViewById(R.id.profile_drawable);
_kropView = findViewById(R.id.krop_view);
_saveImageButton = findViewById(R.id.iv_saveImage);
_textName = findViewById(R.id.text_name);
_textIssuer = findViewById(R.id.text_issuer);
_textPeriod = findViewById(R.id.text_period);
_textPeriodCounter = findViewById(R.id.text_period_counter);
_textPeriodCounterLayout = findViewById(R.id.text_period_counter_layout);
_textDigits = findViewById(R.id.text_digits);
_rowPeriod = findViewById(R.id.row_period);
_textCounter = findViewById(R.id.text_counter);
_rowCounter = findViewById(R.id.row_counter);
_textDigitsLayout = findViewById(R.id.text_digits_layout);
_textSecret = findViewById(R.id.text_secret);
_spinnerType = findViewById(R.id.spinner_type);
SpinnerHelper.fillSpinner(this, _spinnerType, R.array.otp_types_array);
_spinnerAlgo = findViewById(R.id.spinner_algo);
SpinnerHelper.fillSpinner(this, _spinnerAlgo, R.array.otp_algo_array);
_spinnerGroup = findViewById(R.id.spinner_group);
updateGroupSpinnerList();
SpinnerHelper.fillSpinner(this, _spinnerGroup, _spinnerGroupList);
_dropdownType = findViewById(R.id.dropdown_type);
DropdownHelper.fillDropdown(this, _dropdownType, R.array.otp_types_array);
_dropdownAlgoLayout = findViewById(R.id.dropdown_algo_layout);
_dropdownAlgo = findViewById(R.id.dropdown_algo);
DropdownHelper.fillDropdown(this, _dropdownAlgo, R.array.otp_algo_array);
_dropdownGroup = findViewById(R.id.dropdown_group);
updateGroupDropdownList();
DropdownHelper.fillDropdown(this, _dropdownGroup, _dropdownGroupList);
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
if (!_isNew || (_isNew && !_isManual)) {
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
layoutBasic.removeView(layoutSecret);
layoutAdvanced.addView(layoutSecret, 0);
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
if (_isNew && !_isManual) {
setViewEnabled(layoutAdvanced, false);
}
} else {
LinearLayout layoutTypeAlgo = findViewById(R.id.layout_type_algo);
((LinearLayout.LayoutParams) layoutTypeAlgo.getLayoutParams()).topMargin = 0;
}
_advancedSettingsHeader = findViewById(R.id.accordian_header);
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
_advancedSettings = findViewById(R.id.expandableLayout);
// fill the fields with values if possible
if (_origEntry.hasIcon()) {
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
Glide.with(this)
.asDrawable()
.load(_origEntry)
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
@@ -159,11 +201,11 @@ public class EditEntryActivity extends AegisActivity {
OtpInfo info = _origEntry.getInfo();
if (info instanceof TotpInfo) {
_textPeriod.setText(Integer.toString(((TotpInfo) info).getPeriod()));
_rowPeriod.setVisibility(View.VISIBLE);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
} else if (info instanceof HotpInfo) {
_textCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
_rowCounter.setVisibility(View.VISIBLE);
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(Long.toString(((HotpInfo) info).getCounter()));
} else {
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
}
@@ -175,11 +217,9 @@ public class EditEntryActivity extends AegisActivity {
_textSecret.setText(secretString);
}
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);
_dropdownType.setText(_origEntry.getInfo().getType(), false);
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
String group = _origEntry.getGroup();
setGroup(group);
@@ -189,85 +229,72 @@ public class EditEntryActivity extends AegisActivity {
_textName.addTextChangedListener(_iconChangeListener);
// show/hide period and counter fields on type change
_spinnerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String type = _spinnerType.getSelectedItem().toString().toLowerCase();
switch (type) {
case TotpInfo.ID:
case SteamInfo.ID:
_rowCounter.setVisibility(View.GONE);
_rowPeriod.setVisibility(View.VISIBLE);
break;
case HotpInfo.ID:
_rowPeriod.setVisibility(View.GONE);
_rowCounter.setVisibility(View.VISIBLE);
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
_dropdownType.setOnItemClickListener((parent, view, position, id) -> {
String type = _dropdownType.getText().toString().toLowerCase();
switch (type) {
case SteamInfo.ID:
_dropdownAlgo.setText(OtpInfo.DEFAULT_ALGORITHM, false);
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(SteamInfo.DIGITS));
break;
case TotpInfo.ID:
_textPeriodCounterLayout.setHint(R.string.period_hint);
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
case HotpInfo.ID:
_textPeriodCounterLayout.setHint(R.string.counter);
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
break;
default:
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
updateAdvancedFieldStatus(type);
});
final Activity activity = this;
_spinnerGroup.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
private int prevPosition;
_iconView.setOnClickListener(v -> {
startIconSelection();
});
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
private int prevPosition = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position == _spinnerGroupList.size() - 1) {
Dialogs.showTextInputDialog(activity, R.string.set_group, R.string.group_name_hint, text -> {
String str = new String(text);
if (str.isEmpty()) {
return;
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (position == _dropdownGroupList.size() - 1) {
Dialogs.showTextInputDialog(EditEntryActivity.this, R.string.set_group, R.string.group_name_hint, text -> {
String groupName = new String(text);
if (!groupName.isEmpty()) {
_groups.add(groupName);
updateGroupDropdownList();
_dropdownGroup.setText(groupName, false);
}
_groups.add(str);
// reset the selection to "No group" to work around a quirk
_spinnerGroup.setSelection(0, false);
updateGroupSpinnerList();
_spinnerGroup.setSelection(_spinnerGroupList.indexOf(str), false);
});
_spinnerGroup.setSelection(prevPosition, false);
_dropdownGroup.setText(_dropdownGroupList.get(prevPosition), false);
} else {
prevPosition = position;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
_iconView.setOnClickListener(v -> {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, "Select photo");
startActivityForResult(Intent.createChooser(chooserIntent, "Select photo"), PICK_IMAGE_REQUEST);
});
_advancedSettingsHeader.setOnClickListener(v -> openAdvancedSettings());
// automatically open advanced settings since 'Secret' is required.
if (_isNew) {
openAdvancedSettings();
setGroup(selectedGroup);
}
private void updateAdvancedFieldStatus(String otpType) {
boolean enabled = !otpType.equals(SteamInfo.ID) && (!_isNew || _isManual);
_textDigitsLayout.setEnabled(enabled);
_textPeriodCounterLayout.setEnabled(enabled);
_dropdownAlgoLayout.setEnabled(enabled);
}
private void setGroup(String groupName) {
if (groupName == null) {
return;
int pos = 0;
if (groupName != null) {
pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() + 1 : 0;
}
int pos = _groups.contains(groupName) ? _groups.headSet(groupName).size() : -1;
_spinnerGroup.setSelection(pos + 1, false);
_dropdownGroup.setText(_dropdownGroupList.get(pos), false);
}
private void openAdvancedSettings() {
@@ -316,16 +343,21 @@ public class EditEntryActivity extends AegisActivity {
});
}
private void updateGroupSpinnerList() {
private void updateGroupDropdownList() {
Resources res = getResources();
_spinnerGroupList.clear();
_spinnerGroupList.add(res.getString(R.string.no_group));
_spinnerGroupList.addAll(_groups);
_spinnerGroupList.add(res.getString(R.string.new_group));
_dropdownGroupList.clear();
_dropdownGroupList.add(res.getString(R.string.no_group));
_dropdownGroupList.addAll(_groups);
_dropdownGroupList.add(res.getString(R.string.new_group));
}
@Override
public void onBackPressed() {
if (_isEditingIcon) {
stopEditingIcon(false);
return;
}
AtomicReference<String> msg = new AtomicReference<>();
AtomicReference<VaultEntry> entry = new AtomicReference<>();
@@ -366,13 +398,18 @@ public class EditEntryActivity extends AegisActivity {
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
Dialogs.showDeleteEntriesDialog(this, Collections.singletonList(_origEntry.getIssuer()), (dialog, which) -> {
deleteAndFinish(_origEntry);
});
break;
case R.id.action_edit_icon:
startIconSelection();
break;
case R.id.action_default_icon:
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
_iconView.setImageDrawable(drawable);
_selectedIcon = null;
_hasCustomIcon = false;
_hasChangedIcon = true;
default:
@@ -382,6 +419,95 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void startImageSelectionActivity() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
fileIntent.setType("image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_icon));
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
AegisActivity.Helper.startExtActivityForResult(this, chooserIntent, PICK_IMAGE_REQUEST);
}
private void startIconSelection() {
List<IconPack> iconPacks = getApp().getIconPackManager().getIconPacks().stream()
.sorted(Comparator.comparing(IconPack::getName))
.collect(Collectors.toList());
if (iconPacks.size() == 0) {
startImageSelectionActivity();
return;
}
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
selectIcon(icon);
}
@Override
public void onCustomSelected() {
startImageSelectionActivity();
}
});
Dialogs.showSecureDialog(dialog);
}
private void selectIcon(IconPack.Icon icon) {
_selectedIcon = icon;
_hasCustomIcon = true;
_hasChangedIcon = true;
IconViewHelper.setLayerType(_iconView, icon.getIconType());
Glide.with(EditEntryActivity.this)
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false)
.into(_iconView);
}
private void startEditingIcon(Uri data) {
Glide.with(this)
.asBitmap()
.load(data)
.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(v -> {
stopEditingIcon(true);
});
_isEditingIcon = true;
}
private void stopEditingIcon(boolean save) {
if (save && _selectedIcon == null) {
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
}
_iconView.setVisibility(View.VISIBLE);
_kropView.setVisibility(View.GONE);
_hasCustomIcon = _hasCustomIcon || save;
_hasChangedIcon = save;
_isEditingIcon = false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit, menu);
@@ -396,7 +522,11 @@ public class EditEntryActivity extends AegisActivity {
}
private void addAndFinish(VaultEntry entry) {
if (_isNew) {
// It's possible that the new entry was already added to the vault, but writing the
// vault to disk failed, causing the user to tap 'Save' again. Calling addEntry
// again would cause a crash in that case, so the isEntryDuplicate check prevents
// that.
if (_isNew && !_vault.isEntryDuplicate(entry)) {
_vault.addEntry(entry);
} else {
_vault.replaceEntry(entry);
@@ -415,7 +545,7 @@ public class EditEntryActivity extends AegisActivity {
intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete);
if (saveVault()) {
if (saveVault(true)) {
setResult(RESULT_OK, intent);
finish();
}
@@ -424,43 +554,46 @@ 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) {
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) {
String fileType = getMimeType(data.getData());
if (fileType != null && fileType.equals(IconType.SVG.toMimeType())) {
ImportFileTask.Params params = new ImportFileTask.Params(data.getData(), "icon", null);
ImportFileTask task = new ImportFileTask(this, result -> {
if (result.getException() == null) {
CustomSvgIcon icon = new CustomSvgIcon(result.getFile());
selectIcon(icon);
} else {
Dialogs.showErrorDialog(this, R.string.reading_file_error, result.getException());
}
});
_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;
}
});
task.execute(getLifecycle(), params);
} else {
startEditingIcon(data.getData());
}
}
super.onActivityResult(requestCode, resultCode, data);
}
private String getMimeType(Uri uri) {
DocumentFile file = DocumentFile.fromSingleUri(this, uri);
if (file != null) {
String fileType = file.getType();
if (fileType != null) {
return fileType;
}
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (ext != null) {
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
}
return null;
}
private int parsePeriod() throws ParseException {
try {
return Integer.parseInt(_textPeriod.getText().toString());
return Integer.parseInt(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Period is not an integer.");
}
@@ -471,8 +604,8 @@ public class EditEntryActivity extends AegisActivity {
throw new ParseException("Secret is a required field.");
}
String type = _spinnerType.getSelectedItem().toString();
String algo = _spinnerAlgo.getSelectedItem().toString();
String type = _dropdownType.getText().toString();
String algo = _dropdownAlgo.getText().toString();
int digits;
try {
@@ -483,7 +616,8 @@ public class EditEntryActivity extends AegisActivity {
byte[] secret;
try {
secret = Base32.decode(new String(EditTextHelper.getEditTextChars(_textSecret, true)));
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
secret = GoogleAuthInfo.parseSecret(secretString);
if (secret.length == 0) {
throw new ParseException("Secret cannot be empty");
}
@@ -503,7 +637,7 @@ public class EditEntryActivity extends AegisActivity {
case HotpInfo.ID:
long counter;
try {
counter = Long.parseLong(_textCounter.getText().toString());
counter = Long.parseLong(_textPeriodCounter.getText().toString());
} catch (NumberFormatException e) {
throw new ParseException("Counter is not an integer.");
}
@@ -524,9 +658,9 @@ public class EditEntryActivity extends AegisActivity {
entry.setIssuer(_textIssuer.getText().toString());
entry.setName(_textName.getText().toString());
int groupPos = _spinnerGroup.getSelectedItemPosition();
int groupPos = _dropdownGroupList.indexOf(_dropdownGroup.getText().toString());
if (groupPos != 0) {
String group = _spinnerGroupList.get(_spinnerGroup.getSelectedItemPosition());
String group = _dropdownGroupList.get(groupPos);
entry.setGroup(group);
} else {
entry.setGroup(null);
@@ -534,13 +668,25 @@ public class EditEntryActivity extends AegisActivity {
if (_hasChangedIcon) {
if (_hasCustomIcon) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data);
if (_selectedIcon == null) {
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// the quality parameter is ignored for PNG
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] data = stream.toByteArray();
entry.setIcon(data, IconType.PNG);
} else {
byte[] iconBytes;
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
iconBytes = IOUtils.readFile(inStream);
} catch (IOException e) {
throw new ParseException(e.getMessage());
}
entry.setIcon(iconBytes, _selectedIcon.getIconType());
}
} else {
entry.setIcon(null);
entry.setIcon(null, IconType.INVALID);
}
}
@@ -556,6 +702,10 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
if (_isEditingIcon) {
stopEditingIcon(true);
}
VaultEntry entry;
try {
entry = parseEntry();
@@ -568,7 +718,18 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private TextWatcher _iconChangeListener = new TextWatcher() {
private static void setViewEnabled(View view, boolean enabled) {
view.setEnabled(enabled);
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0; i < group.getChildCount(); i++) {
setViewEnabled(group.getChildAt(i), enabled);
}
}
}
private final TextWatcher _iconChangeListener = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@@ -586,19 +747,28 @@ 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].equalsIgnoreCase(string)) {
return i;
}
}
return -1;
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
private static class CustomSvgIcon extends IconPack.Icon {
private final File _file;
protected CustomSvgIcon(File file) {
super(file.getAbsolutePath(), null, null);
_file = file;
}
@Nullable
public File getFile() {
return _file;
}
@Override
public IconType getIconType() {
return IconType.SVG;
}
}
}
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
public class ExitActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
}
public static void exitAppAndRemoveFromRecents(Context context) {
Intent intent = new Intent(context, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_NO_ANIMATION |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
context.startActivity(intent);
}
}
@@ -3,47 +3,65 @@ package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
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.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.views.GroupAdapter;
import java.text.Collator;
import java.util.ArrayList;
import java.util.TreeSet;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class GroupManagerActivity extends AegisActivity implements GroupAdapter.Listener {
private GroupAdapter _adapter;
private TreeSet<String> _groups;
private RecyclerView _slotsView;
private View _emptyStateView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_groups);
setSupportActionBar(findViewById(R.id.toolbar));
Intent intent = getIntent();
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
// set up the recycler view
_adapter = new GroupAdapter(this);
RecyclerView slotsView = findViewById(R.id.list_slots);
_slotsView= findViewById(R.id.list_slots);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
slotsView.setLayoutManager(layoutManager);
slotsView.setAdapter(_adapter);
slotsView.setNestedScrollingEnabled(false);
_slotsView.setLayoutManager(layoutManager);
_slotsView.setAdapter(_adapter);
_slotsView.setNestedScrollingEnabled(false);
for (String group : _groups) {
_adapter.addGroup(group);
}
_emptyStateView = findViewById(R.id.vEmptyList);
updateEmptyState();
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_slotsView.setVisibility(View.VISIBLE);
_emptyStateView.setVisibility(View.GONE);
} else {
_slotsView.setVisibility(View.GONE);
_emptyStateView.setVisibility(View.VISIBLE);
}
}
@Override
@@ -54,6 +72,7 @@ public class GroupManagerActivity extends AegisActivity implements GroupAdapter.
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
_groups.remove(group);
_adapter.removeGroup(group);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
@@ -0,0 +1,271 @@
package com.beemdevelopment.aegis.ui;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class ImportEntriesActivity extends AegisActivity {
private Menu _menu;
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_import_entries);
setSupportActionBar(findViewById(R.id.toolbar));
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);
_fabScrollHelper.onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
if (getApp().getVaultManager().getEntries().size() > 0
&& _menu.findItem(R.id.toggle_wipe_vault).isChecked()) {
showWipeEntriesDialog();
} else {
saveAndFinish(false);
}
});
_fabScrollHelper = new FabScrollHelper(fab);
Class<? extends DatabaseImporter> importerType = (Class<? extends DatabaseImporter>) getIntent().getSerializableExtra("importerType");
startImport(importerType, (File) getIntent().getSerializableExtra("file"));
}
private void startImport(@NonNull Class<? extends DatabaseImporter> importerType, @Nullable File file) {
if (file == null) {
startImportApp(importerType);
} else {
startImportFile(importerType, file);
}
}
private void startImportFile(@NonNull Class<? extends DatabaseImporter> importerType, @NonNull File file) {
try (InputStream stream = new FileInputStream(file)) {
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
DatabaseImporter.State state = importer.read(stream);
processImporterState(state);
} catch (FileNotFoundException e) {
Toast.makeText(this, R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (DatabaseImporterException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
}
}
private void startImportApp(@NonNull Class<? extends DatabaseImporter> importerType) {
DatabaseImporter importer = DatabaseImporter.create(this, importerType);
// 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(this, R.string.root_error, Toast.LENGTH_SHORT).show();
finish();
return;
}
DatabaseImporter.State state = importer.readFromApp();
processImporterState(state);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
Toast.makeText(this, R.string.app_lookup_error, Toast.LENGTH_SHORT).show();
finish();
} catch (IOException | DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.reading_file_error, e, (dialog, which) -> finish());
}
}
private void processImporterState(DatabaseImporter.State state) {
try {
if (state.isEncrypted()) {
state.decrypt(this, new DatabaseImporter.DecryptListener() {
@Override
public void onStateDecrypted(DatabaseImporter.State state) {
importDatabase(state);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(ImportEntriesActivity.this, R.string.decryption_error, e, (dialog, which) -> finish());
}
@Override
public void onCanceled() {
finish();
}
});
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
}
}
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.parsing_file_error, e, (dialog, which) -> finish());
return;
}
UUIDMap<VaultEntry> entries = result.getEntries();
for (VaultEntry entry : entries.getValues()) {
_adapter.addEntry(new ImportEntry(entry));
}
List<DatabaseImporterEntryException> errors = result.getErrors();
if (errors.size() > 0) {
showErrorDialog(errors);
}
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getResources().getQuantityString(R.plurals.import_error_dialog, errors.size(), 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 showWipeEntriesDialog() {
Dialogs.showCheckboxDialog(this, R.string.dialog_wipe_entries_title,
R.string.dialog_wipe_entries_message,
R.string.dialog_wipe_entries_checkbox,
this::saveAndFinish
);
}
private void saveAndFinish(boolean wipeEntries) {
VaultManager vault = getApp().getVaultManager();
if (wipeEntries) {
vault.wipeEntries();
}
List<ImportEntry> selectedEntries = _adapter.getCheckedEntries();
for (ImportEntry selectedEntry : selectedEntries) {
VaultEntry entry = selectedEntry.getEntry();
// temporary: randomize the UUID of duplicate entries and add them anyway
if (vault.isEntryDuplicate(entry)) {
entry.resetUUID();
}
vault.addEntry(entry);
}
if (saveVault(true)) {
String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size());
Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show();
setResult(RESULT_OK, null);
finish();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_import_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;
case R.id.toggle_wipe_vault:
item.setChecked(!item.isChecked());
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}
@@ -1,103 +1,83 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.intro.IntroBaseActivity;
import com.beemdevelopment.aegis.ui.intro.SlideFragment;
import com.beemdevelopment.aegis.ui.slides.DoneSlide;
import com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide;
import com.beemdevelopment.aegis.ui.slides.SecuritySetupSlide;
import com.beemdevelopment.aegis.ui.slides.WelcomeSlide;
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.github.appintro.AppIntro2;
import com.github.appintro.AppIntroFragment;
import com.github.appintro.model.SliderPage;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import org.json.JSONObject;
public class IntroActivity extends AppIntro2 {
private CustomAuthenticatedSlide _authenticatedSlide;
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private AegisApplication _app;
private Preferences _prefs;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_BIOMETRIC;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_INVALID;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_NONE;
import static com.beemdevelopment.aegis.ui.slides.SecurityPickerSlide.CRYPT_TYPE_PASS;
public class IntroActivity extends IntroBaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_app = (AegisApplication) getApplication();
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
setWizardMode(true);
setSkipButtonEnabled(false);
showStatusBar(true);
setSystemBackButtonLocked(true);
setBarColor(getResources().getColor(R.color.colorPrimary));
SliderPage homeSliderPage = new SliderPage();
homeSliderPage.setTitle(getString(R.string.welcome));
homeSliderPage.setImageDrawable(R.drawable.app_icon);
homeSliderPage.setTitleColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setDescription(getString(R.string.app_description));
homeSliderPage.setDescriptionColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_authenticationSlide = new CustomAuthenticationSlide();
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticationSlide);
_authenticatedSlide = new CustomAuthenticatedSlide();
_authenticatedSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(_authenticatedSlide);
SliderPage endSliderPage = new SliderPage();
endSliderPage.setTitle(getString(R.string.setup_completed));
endSliderPage.setDescription(getString(R.string.setup_completed_description));
endSliderPage.setImageDrawable(R.drawable.app_icon);
endSliderPage.setBackgroundColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
addSlide(WelcomeSlide.class);
addSlide(SecurityPickerSlide.class);
addSlide(SecuritySetupSlide.class);
addSlide(DoneSlide.class);
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
int cryptType = getIntent().getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
goToNextSlide(false);
}
}
setSwipeLock(true);
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
@Override
public void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
protected boolean onBeforeSlideChanged(Class<? extends SlideFragment> oldSlide, Class<? extends SlideFragment> newSlide) {
// hide the keyboard before every slide change
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(findViewById(android.R.id.content).getWindowToken(), 0);
int cryptType = _authenticatedSlide.getCryptType();
VaultFileCredentials creds = _authenticatedSlide.getCredentials();
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
skipToSlide(DoneSlide.class);
return true;
}
return false;
}
@Override
protected void onDonePressed() {
Bundle state = getState();
int cryptType = state.getInt("cryptType", CRYPT_TYPE_INVALID);
VaultFileCredentials creds = (VaultFileCredentials) state.getSerializable("creds");
if (cryptType == CRYPT_TYPE_INVALID
|| (cryptType == CRYPT_TYPE_NONE && creds != null)
|| (cryptType == CRYPT_TYPE_PASS && (creds == null || !creds.getSlots().has(PasswordSlot.class)))
|| (cryptType == CRYPT_TYPE_BIOMETRIC && (creds == null || !creds.getSlots().has(PasswordSlot.class) || !creds.getSlots().has(BiometricSlot.class)))) {
throw new RuntimeException(String.format("State of SecuritySetupSlide not properly propagated, cryptType: %d, creds: %s", cryptType, creds));
}
Vault vault = new Vault();
VaultFile vaultFile = new VaultFile();
try {
JSONObject obj = vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
vaultFile.setContent(obj, creds);
@@ -110,20 +90,16 @@ public class IntroActivity extends AppIntro2 {
return;
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_app.initVaultManager(vault, null);
if (cryptType == CRYPT_TYPE_NONE) {
getApp().initVaultManager(vault, null);
} else {
_app.initVaultManager(vault, creds);
getApp().initVaultManager(vault, creds);
}
// skip the intro from now on
_prefs.setIntroDone(true);
getPreferences().setIntroDone(true);
setResult(RESULT_OK);
finish();
}
public void goToNextSlide() {
super.goToNextSlide(false);
}
}
@@ -7,52 +7,58 @@ import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.helpers.BitmapHelper;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
import com.beemdevelopment.aegis.ui.views.EntryListView;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
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.google.zxing.qrcode.QRCodeReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
public class MainActivity extends AegisActivity implements EntryListView.Listener {
// activity request codes
@@ -71,16 +77,18 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private AegisApplication _app;
private VaultManager _vault;
private boolean _loaded;
private String _selectedGroup;
private boolean _searchSubmitted;
private boolean _isAuthenticating;
private boolean _isDoingIntro;
private List<VaultEntry> _selectedEntries;
private ActionMode _actionMode;
private Menu _menu;
private SearchView _searchView;
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private LinearLayout _btnBackupError;
private FabScrollHelper _fabScrollHelper;
@@ -89,82 +97,78 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(findViewById(R.id.toolbar));
_app = (AegisApplication) getApplication();
_vault = _app.getVaultManager();
_loaded = false;
// set up the main view
setContentView(R.layout.activity_main);
if (savedInstanceState != null) {
_isAuthenticating = savedInstanceState.getBoolean("isAuthenticating");
_isDoingIntro = savedInstanceState.getBoolean("isDoingIntro");
}
// set up the entry view
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
_entryListView.setListener(this);
_entryListView.setCodeGroupSize(getPreferences().getCodeGroupSize());
_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());
_entryListView.setIsCopyOnTapEnabled(getPreferences().isCopyOnTapEnabled());
_entryListView.setPrefGroupFilter(getPreferences().getGroupFilter());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
startEditEntryActivity(CODE_ADD_ENTRY, null, true);
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
startScanImageActivity();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> {
View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null);
BottomSheetDialog dialog = new BottomSheetDialog(this);
dialog.setContentView(view);
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
dialog.dismiss();
startEditEntryActivityForManual(CODE_ADD_ENTRY);
});
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
dialog.dismiss();
startScanImageActivity();
});
view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> {
dialog.dismiss();
startScanActivity();
});
Dialogs.showSecureDialog(dialog);
});
_btnBackupError = findViewById(R.id.btn_backup_error);
_btnBackupError.setOnClickListener(view -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
_fabScrollHelper = new FabScrollHelper(fab);
_selectedEntries = new ArrayList<>();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isAuthenticating", _isAuthenticating);
outState.putBoolean("isDoingIntro", _isDoingIntro);
}
@Override
protected void onDestroy() {
_entryListView.setListener(null);
super.onDestroy();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// collapse the fab menu on touch
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (_fabMenu.isExpanded()) {
Rect rect = new Rect();
_fabMenu.getGlobalVisibleRect(rect);
if (!rect.contains((int) event.getRawX(), (int) event.getRawY())) {
_fabMenu.collapse();
}
}
}
return super.dispatchTouchEvent(event);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
doShortcutActions();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _app.isVaultLocked()) {
return;
}
_isAuthenticating = false;
_isDoingIntro = false;
if (resultCode != RESULT_OK) {
return;
@@ -215,71 +219,91 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void onPreferencesResult(Intent data) {
// refresh the entire entry list if needed
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
if (_loaded) {
if (data.getBooleanExtra("needsRecreate", false)) {
recreate();
} else if (data.getBooleanExtra("needsRefresh", false)) {
boolean showAccountName = getPreferences().isAccountNameVisible();
int codeGroupSize = getPreferences().getCodeGroupSize();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
boolean copyOnTap = getPreferences().isCopyOnTapEnabled();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setCodeGroupSize(codeGroupSize);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.setIsCopyOnTapEnabled(copyOnTap);
_entryListView.refresh(true);
}
}
}
private void startEditEntryActivity(int requestCode, VaultEntry entry, boolean isNew) {
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
if (isNew) {
intent.putExtra("newEntry", entry != null ? entry : VaultEntry.getDefault());
} else {
intent.putExtra("entryUUID", entry.getUUID());
}
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("newEntry", entry);
intent.putExtra("isManual", false);
startActivityForResult(intent, requestCode);
}
private void startEditEntryActivityForManual(int requestCode) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("newEntry", VaultEntry.getDefault());
intent.putExtra("isManual", true);
startActivityForResult(intent, requestCode);
}
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entryUUID", entry.getUUID());
startActivityForResult(intent, requestCode);
}
private void onScanResult(Intent data) {
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
if (entries.size() == 1) {
startEditEntryActivity(CODE_ADD_ENTRY, entries.get(0), true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
} else {
for (VaultEntry entry : entries) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
if (_loaded) {
_entryListView.addEntry(entry);
}
}
saveVault();
saveVault(true);
}
}
private void onAddEntryResult(Intent data) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry);
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry, true);
}
}
private void onEditEntryResult(Intent data) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
private void onScanImageResult(Intent intent) {
Uri inputFile = (intent.getData());
decodeQrCodeImage(intent.getData());
}
private void decodeQrCodeImage(Uri inputFile) {
Bitmap bitmap;
try {
@@ -287,6 +311,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
@@ -295,61 +320,24 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
Reader reader = new MultiFormatReader();
Reader reader = new QRCodeReader();
Result result = reader.decode(binaryBitmap);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
}
private void updateGroupFilterMenu() {
SubMenu menu = _menu.findItem(R.id.action_filter).getSubMenu();
for (int i = menu.size() - 1; i >= 0; i--) {
MenuItem item = menu.getItem(i);
if (item.getItemId() == R.id.menu_filter_all) {
continue;
}
menu.removeItem(item.getItemId());
}
// if the group no longer exists, switch back to 'All'
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(_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 updateSortCategoryMenu() {
SortCategory category = getPreferences().getCurrentSortCategory();
_menu.findItem(category.getMenuItem()).setChecked(true);
}
private void setGroupFilter(String group) {
getSupportActionBar().setSubtitle(group);
_selectedGroup = group;
_entryListView.setGroupFilter(group, true);
}
private void onDoIntroResult() {
_vault = _app.getVaultManager();
loadEntries();
@@ -385,8 +373,23 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
Intent fileIntent = new Intent(Intent.ACTION_GET_CONTENT);
fileIntent.setType("image/*");
Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.select_picture));
startActivityForResult(Intent.createChooser(chooserIntent, getString(R.string.select_picture)), CODE_SCAN_IMAGE);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { fileIntent });
AegisActivity.Helper.startExtActivityForResult(this, chooserIntent, CODE_SCAN_IMAGE);
}
private void startPreferencesActivity() {
startPreferencesActivity(null, null);
}
private void startPreferencesActivity(Class<? extends PreferencesFragment> fragmentType, String preference) {
Intent intent = new Intent(this, PreferencesActivity.class);
intent.putExtra("fragment", fragmentType);
intent.putExtra("pref", preference);
startActivityForResult(intent, CODE_PREFERENCES);
}
private void doShortcutActions() {
@@ -411,10 +414,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
Intent intent = getIntent();
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
getIntent().setData(null);
getIntent().setAction(null);
Uri uri = intent.getData();
if (Intent.ACTION_VIEW.equals(intent.getAction()) && uri != null) {
intent.setData(null);
intent.setAction(null);
GoogleAuthInfo info = null;
try {
@@ -426,23 +429,41 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
if (info != null) {
VaultEntry entry = new VaultEntry(info);
startEditEntryActivity(CODE_ADD_ENTRY, entry, true);
startEditEntryActivityForNew(CODE_ADD_ENTRY, entry);
}
}
}
private void handleSharedImage() {
if (_app.isVaultLocked()) {
return;
}
Intent intent = getIntent();
String action = intent.getAction();
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (Intent.ACTION_SEND.equals(action) && uri != null) {
intent.setAction(null);
intent.removeExtra(Intent.EXTRA_STREAM);
decodeQrCodeImage(uri);
}
}
@Override
protected void onResume() {
super.onResume();
if (_vault == null) {
// start the intro if the vault file doesn't exist
if (!VaultManager.fileExists(this)) {
if (!_isDoingIntro && !VaultManager.fileExists(this)) {
if (getPreferences().isIntroDone()) {
Toast.makeText(this, getString(R.string.vault_not_found), Toast.LENGTH_SHORT).show();
}
Intent intro = new Intent(this, IntroActivity.class);
startActivityForResult(intro, CODE_DO_INTRO);
_isDoingIntro = true;
return;
}
@@ -461,12 +482,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
if (_app.isVaultLocked()) {
startAuthActivity();
startAuthActivity(false);
} else if (_loaded) {
// update the list of groups in the filter menu
if (_menu != null) {
updateGroupFilterMenu();
}
// update the list of groups in the entry list view so that the chip gets updated
_entryListView.setGroups(_vault.getGroups());
// refresh all codes to prevent showing old ones
_entryListView.refresh(false);
@@ -476,8 +495,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
handleDeeplink();
handleSharedImage();
updateLockIcon();
doShortcutActions();
updateBackupErrorBar();
}
@Override
@@ -487,13 +508,12 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.setSearchFilter(null);
collapseSearchView();
setTitle("Aegis");
setGroupFilter(_selectedGroup);
setTitle(R.string.app_name);
return;
}
if (_app.isAutoLockEnabled()) {
_app.lock();
if (_app.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
_app.lock(false);
return;
}
@@ -506,7 +526,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.removeEntry(oldEntry);
}
saveVault();
saveVault(true);
}
@Override
@@ -515,7 +535,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
getMenuInflater().inflate(R.menu.menu_main, menu);
updateLockIcon();
if (_loaded) {
updateGroupFilterMenu();
_entryListView.setGroups(_vault.getGroups());
updateSortCategoryMenu();
}
@@ -556,8 +576,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings: {
Intent intent = new Intent(this, PreferencesActivity.class);
startActivityForResult(intent, CODE_PREFERENCES);
startPreferencesActivity();
return true;
}
case R.id.action_about: {
@@ -566,19 +585,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
}
case R.id.action_lock:
_app.lock();
_app.lock(true);
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
item.setChecked(true);
String group = null;
if (item.getItemId() != R.id.menu_filter_all) {
group = item.getTitle().toString();
}
setGroupFilter(group);
}
if (item.getGroupId() == R.id.action_sort_category) {
item.setChecked(true);
@@ -615,16 +624,20 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void loadEntries() {
List<VaultEntry> entries = new ArrayList<>(_vault.getEntries());
_entryListView.addEntries(entries);
_entryListView.runEntriesAnimation();
_loaded = true;
if (!_loaded) {
_entryListView.addEntries(_vault.getEntries());
_entryListView.runEntriesAnimation();
_loaded = true;
}
}
private void startAuthActivity() {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
private void startAuthActivity(boolean inhibitBioPrompt) {
if (!_isAuthenticating) {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
startActivityForResult(intent, CODE_DECRYPT);
_isAuthenticating = true;
}
}
private void updateLockIcon() {
@@ -635,6 +648,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void updateBackupErrorBar() {
String error = null;
if (_app.getPreferences().isBackupsEnabled()) {
error = _app.getPreferences().getBackupsError();
}
_btnBackupError.setVisibility(error == null ? View.GONE : View.VISIBLE);
}
@Override
public void onEntryClick(VaultEntry entry) {
if (_actionMode != null) {
@@ -646,8 +668,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
copyEntryCode(entry);
}
@Override
@@ -684,12 +704,16 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onEntryDrop(VaultEntry entry) {
saveVault();
saveVault(false);
}
@Override
public void onEntryChange(VaultEntry entry) {
saveVault();
saveVault(true);
}
public void onEntryCopy(VaultEntry entry) {
copyEntryCode(entry);
}
@Override
@@ -698,7 +722,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
@Override
public void onLocked() {
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onSaveGroupFilter(List<String> groupFilter) {
getPreferences().setGroupFilter(groupFilter);
}
@Override
public void onLocked(boolean userInitiated) {
if (_actionMode != null) {
_actionMode.finish();
}
@@ -706,11 +738,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.clearEntries();
_loaded = false;
if (isOpen()) {
startAuthActivity();
if (userInitiated) {
startAuthActivity(true);
} else {
super.onLocked(userInitiated);
}
super.onLocked();
}
private void copyEntryCode(VaultEntry entry) {
@@ -741,24 +773,39 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
case R.id.action_edit:
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0), false);
startEditEntryActivity(CODE_EDIT_ENTRY, _selectedEntries.get(0));
mode.finish();
return true;
case R.id.action_share_qr:
Intent intent = new Intent(getBaseContext(), TransferEntriesActivity.class);
ArrayList<GoogleAuthInfo> authInfos = new ArrayList<>();
for (VaultEntry entry : _selectedEntries) {
GoogleAuthInfo authInfo = new GoogleAuthInfo(entry.getInfo(), entry.getName(), entry.getIssuer());
authInfos.add(authInfo);
}
intent.putExtra("authInfos", authInfos);
startActivity(intent);
mode.finish();
return true;
case R.id.action_delete:
Dialogs.showDeleteEntriesDialog(MainActivity.this, (d, which) -> {
Dialogs.showDeleteEntriesDialog(MainActivity.this, _selectedEntries.stream().map(VaultEntry::getIssuer).collect(Collectors.toList()), (d, which) -> {
deleteEntries(_selectedEntries);
for (VaultEntry entry : _selectedEntries) {
if (entry.getGroup() != null) {
if (!_vault.getGroups().contains(entry.getGroup())) {
updateGroupFilterMenu();
_entryListView.setGroups(_vault.getGroups());
break;
}
}
}
mode.finish();
}, _selectedEntries.size());
});
return true;
default:
return false;
@@ -0,0 +1,47 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.pins.GuardianProjectFDroidRSA2048;
import com.beemdevelopment.aegis.vault.VaultManager;
import info.guardianproject.GuardianProjectRSA4096;
import info.guardianproject.trustedintents.TrustedIntents;
public class PanicResponderActivity extends AegisActivity {
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Preferences prefs = getPreferences();
if(!prefs.isPanicTriggerEnabled()) {
Toast.makeText(this, R.string.panic_trigger_ignore_toast, Toast.LENGTH_SHORT).show();
finish();
}
TrustedIntents trustedIntents = TrustedIntents.get(this);
trustedIntents.addTrustedSigner(GuardianProjectRSA4096.class);
trustedIntents.addTrustedSigner(GuardianProjectFDroidRSA2048.class);
Intent intent = trustedIntents.getIntentFromTrustedSender(this);
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
getApp().lock(false);
VaultManager.deleteFile(this);
finishApp();
}
finish();
}
private void finishApp() {
ExitActivity.exitAppAndRemoveFromRecents(this);
finishAndRemoveTask();
}
}
@@ -1,43 +1,124 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.MenuItem;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.fragments.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
public class PreferencesActivity extends AegisActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private Fragment _fragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_preferences);
setSupportActionBar(findViewById(R.id.toolbar));
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (savedInstanceState == null) {
_fragment = new PreferencesFragment();
_fragment = new MainPreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, _fragment)
.commit();
PreferencesFragment requestedFragment = getRequestedFragment();
if (requestedFragment != null) {
_fragment = requestedFragment;
showFragment(_fragment);
}
} else {
_fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content);
_fragment = getSupportFragmentManager().findFragmentById(R.id.content);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
public void onBackPressed() {
super.onBackPressed();
setTitle(R.string.action_settings);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// pass permission request results to the fragment
_fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onRestoreInstanceState(final Bundle inState) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
_fragment.setResult(inState.getParcelable("result"));
if (_fragment instanceof PreferencesFragment) {
// pass the stored result intent back to the fragment
if (inState.containsKey("result")) {
((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
}
}
super.onRestoreInstanceState(inState);
}
@Override
protected void onSaveInstanceState(final Bundle outState) {
// save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", _fragment.getResult());
if (_fragment instanceof PreferencesFragment) {
// save the result intent of the fragment
// this is done so we don't lose anything if the fragment calls recreate on this activity
outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
}
super.onSaveInstanceState(outState);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
_fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
_fragment.setArguments(pref.getExtras());
_fragment.setTargetFragment(caller, 0);
showFragment(_fragment);
setTitle(pref.getTitle());
return true;
}
private void showFragment(Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
.replace(R.id.content, fragment)
.addToBackStack(null)
.commit();
}
@SuppressWarnings("unchecked")
private PreferencesFragment getRequestedFragment() {
Class<? extends PreferencesFragment> fragmentType = (Class<? extends PreferencesFragment>) getIntent().getSerializableExtra("fragment");
if (fragmentType == null) {
return null;
}
try {
return fragmentType.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,895 +0,0 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
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.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
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.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
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.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
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;
private static final int CODE_EXPORT = 5;
private static final int CODE_EXPORT_ENCRYPT = 6;
private static final int CODE_BACKUPS = 7;
private Intent _result;
private Preferences _prefs;
private VaultManager _vault;
// 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 _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _groupsPreference;
private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_prefs = app.getPreferences();
_vault = app.getVaultManager();
// set the result intent in advance
setResult(new Intent());
int currentTheme = _prefs.getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
int currentTheme = _prefs.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();
_prefs.setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference langPreference = findPreference("pref_lang");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
} else {
// Setting locale doesn't work on Marshmallow or below
langPreference.setVisible(false);
}
int currentViewMode = _prefs.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 = _prefs.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();
_prefs.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) {
startImport();
return true;
}
});
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) {
startExport();
return true;
}
});
/*EditTextPreference timeoutPreference = (EditTextPreference) findPreference("pref_timeout");
timeoutPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
preference.setSummary(String.format(getString(R.string.pref_timeout_summary), (String) newValue));
return true;
}
});
timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());*/
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
return true;
});
Preference issuerPreference = findPreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRefresh", true);
return true;
}
});
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
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean)newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
return true;
}
});
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 (!_vault.isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.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 {
_vault.disableEncryption();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
return;
}
// clear the KeyStore
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.clear();
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
})
.setNegativeButton(android.R.string.no, null)
.create());
}
return false;
}
});
_biometricsPreference = (SwitchPreference) findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
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 biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
_vault.setCredentials(creds);
// remove the KeyStore key
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.deleteKey(slot.getUUID().toString());
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
saveVault();
updateEncryptionPreferences();
}
return false;
}
});
_setPasswordPreference = findPreference("pref_password");
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
return false;
});
_slotsPreference = findPreference("pref_slots");
_slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", _vault.getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
}
});
_groupsPreference = findPreference("pref_groups");
_groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
}
});
_backupsPreference = findPreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
} else {
_prefs.setIsBackupsEnabled(false);
updateBackupPreference();
}
return false;
});
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = findPreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
return false;
});
_backupsTriggerPreference = findPreference("pref_backups_trigger");
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (_prefs.isBackupsEnabled()) {
try {
_vault.backup();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
}
}
return true;
});
_backupsVersionsPreference = findPreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
number = number * 5 + 5;
_prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
});
return false;
});
_autoLockPreference = findPreference("pref_auto_lock");
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
updateEncryptionPreferences();
updateBackupPreference();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data == null) {
return;
}
switch (requestCode) {
case CODE_IMPORT:
onImportResult(resultCode, data);
break;
case CODE_IMPORT_DECRYPT:
onImportDecryptResult(resultCode, data);
break;
case CODE_SLOTS:
onSlotManagerResult(resultCode, data);
break;
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
break;
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_ENCRYPT:
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
break;
case CODE_BACKUPS:
onSelectBackupsLocationResult(resultCode, data);
break;
}
}
public Intent getResult() {
return _result;
}
public void setResult(Intent result) {
_result = result;
getActivity().setResult(Activity.RESULT_OK, _result);
}
private void startImport() {
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
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, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_importerType = importers.get(names[i]);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
}
})
.create());
}
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();
}
}
private void importDatabase(DatabaseImporter importer, DatabaseImporter.FileReader reader) {
try {
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) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
}
});
}
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
}
}
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();
Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e);
return;
}
importDatabase(state);
_importerState = null;
}
private void onImportResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
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(), R.string.file_not_found, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e);
}
}
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e);
return;
}
_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 startExport() {
// 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(R.string.pref_export_summary)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String filename = checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN;
filename = new VaultBackupManager.FileInfo(filename).toString();
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, filename);
startActivityForResult(intent, checked.get() ? CODE_EXPORT_ENCRYPT : CODE_EXPORT);
})
.setNegativeButton(android.R.string.cancel, null);
if (_vault.isEncryptionEnabled()) {
final String[] items = {"Keep the vault encrypted"};
final boolean[] checkedItems = {true};
builder.setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int index, boolean isChecked) {
checked.set(isChecked);
}
});
} else {
builder.setMessage(R.string.export_warning);
}
Dialogs.showSecureDialog(builder.create());
}
private void onSlotManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
_vault.setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
private void onGroupManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (VaultEntry entry : _vault.getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
}
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 void onExportResult(int resultCode, Intent data, boolean encrypt) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
try (OutputStream stream = getContext().getContentResolver().openOutputStream(uri, "w")) {
_vault.export(stream, encrypt);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
_prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
}
private boolean saveVault() {
try {
_vault.save();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
return false;
}
return true;
}
private void updateEncryptionPreferences() {
boolean encrypted = _vault.isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = _vault.getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
} else {
_setPasswordPreference.setEnabled(false);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
_passwordReminderPreference.setVisible(false);
}
updateBackupPreference();
}
private void updateBackupPreference() {
boolean encrypted = _vault.isEncryptionEnabled();
boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted;
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
startActivityForResult(intent, CODE_BACKUPS);
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = _vault.getCredentials();
SlotList slots = creds.getSlots();
try {
// encrypt the master key for this slot
slot.setKey(creds.getKey(), cipher);
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
slots.remove(oldSlot);
// add the new master password slot
slots.add(slot);
} catch (SlotException e) {
onException(e);
return;
}
_vault.setCredentials(creds);
saveVault();
}
@Override
public void onException(Exception e) {
e.printStackTrace();
updateEncryptionPreferences();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
VaultFileCredentials creds = _vault.getCredentials();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
e.printStackTrace();
onSlotInitializationFailed(0, e.toString());
return;
}
creds.getSlots().add(slot);
_vault.setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
_vault.enableEncryption(creds);
} catch (VaultManagerException | SlotException e) {
onException(e);
return;
}
getActivity().startService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
}
@@ -1,133 +1,188 @@
package com.beemdevelopment.aegis.ui;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.helpers.SquareFinderView;
import com.beemdevelopment.aegis.ThemeMap;
import com.beemdevelopment.aegis.helpers.QrCodeAnalyzer;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.zxing.BarcodeFormat;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import me.dm7.barcodescanner.core.IViewFinder;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
public class ScannerActivity extends AegisActivity implements QrCodeAnalyzer.Listener {
private ProcessCameraProvider _cameraProvider;
private ListenableFuture<ProcessCameraProvider> _cameraProviderFuture;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
private List<Integer> _lenses;
private int _currentLens;
public class ScannerActivity extends AegisActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView _scannerView;
private Menu _menu;
private int _facing = CAMERA_FACING_BACK;
private ImageAnalysis _analysis;
private PreviewView _previewView;
private ExecutorService _executor;
private int _batchId = 0;
private int _batchIndex = -1;
private List<VaultEntry> _entries;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scanner);
setSupportActionBar(findViewById(R.id.toolbar));
_entries = new ArrayList<>();
_scannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
return new SquareFinderView(context);
}
};
_scannerView.setResultHandler(this);
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
_lenses = new ArrayList<>();
_previewView = findViewById(R.id.preview_view);
_executor = Executors.newSingleThreadExecutor();
int camera = getRearCameraId();
if (camera == -1) {
camera = getFrontCameraId();
if (camera == -1) {
_cameraProviderFuture = ProcessCameraProvider.getInstance(this);
_cameraProviderFuture.addListener(() -> {
try {
_cameraProvider = _cameraProviderFuture.get();
} catch (ExecutionException | InterruptedException e) {
// if we're to believe the Android documentation, this should never happen
// https://developer.android.com/training/camerax/preview#check-provider
throw new RuntimeException(e);
}
addCamera(CameraSelector.LENS_FACING_BACK);
addCamera(CameraSelector.LENS_FACING_FRONT);
if (_lenses.size() == 0) {
Toast.makeText(this, getString(R.string.no_cameras_available), Toast.LENGTH_LONG).show();
finish();
return;
}
_facing = CAMERA_FACING_FRONT;
}
_scannerView.startCamera(camera);
_currentLens = _lenses.get(0);
updateCameraIcon();
setContentView(_scannerView);
bindPreview(_cameraProvider);
}, ContextCompat.getMainExecutor(this));
}
@Override
protected void setPreferredTheme(Theme theme) {
setTheme(R.style.AppTheme_Fullscreen);
protected void onDestroy() {
_executor.shutdownNow();
super.onDestroy();
}
@Override
protected void onSetTheme() {
setTheme(ThemeMap.FULLSCREEN);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
_menu = menu;
getMenuInflater().inflate(R.menu.menu_scanner, menu);
updateCameraIcon();
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_camera:
_scannerView.stopCamera();
switch (_facing) {
case CAMERA_FACING_BACK:
_facing = CAMERA_FACING_FRONT;
break;
case CAMERA_FACING_FRONT:
_facing = CAMERA_FACING_BACK;
break;
}
updateCameraIcon();
_scannerView.startCamera(getCameraId(_facing));
return true;
case R.id.action_lock:
default:
return super.onOptionsItemSelected(item);
if (item.getItemId() == R.id.action_camera) {
unbindPreview(_cameraProvider);
_currentLens = _currentLens == CameraSelector.LENS_FACING_BACK ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
bindPreview(_cameraProvider);
updateCameraIcon();
return true;
}
return super.onOptionsItemSelected(item);
}
private void addCamera(int lens) {
try {
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
if (_cameraProvider.hasCamera(camera)) {
_lenses.add(lens);
}
} catch (CameraInfoUnavailableException e) {
e.printStackTrace();
}
}
@Override
public void onResume() {
super.onResume();
_scannerView.startCamera(getCameraId(_facing));
}
@Override
public void onPause() {
super.onPause();
_scannerView.stopCamera();
}
@Override
public void handleResult(Result rawResult) {
try {
Uri uri = Uri.parse(rawResult.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = _lenses.size() > 1;
if (dual) {
switch (_currentLens) {
case CameraSelector.LENS_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CameraSelector.LENS_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
}
item.setVisible(dual);
}
}
_scannerView.resumeCameraPreview(this);
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.read_qr_error, e, (dialog, which) -> {
_scannerView.resumeCameraPreview(this);
});
private void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(_previewView.getSurfaceProvider());
CameraSelector selector = new CameraSelector.Builder()
.requireLensFacing(_currentLens)
.build();
_analysis = new ImageAnalysis.Builder()
.setTargetResolution(QrCodeAnalyzer.RESOLUTION)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build();
_analysis.setAnalyzer(_executor, new QrCodeAnalyzer(this));
cameraProvider.bindToLifecycle(this, selector, preview, _analysis);
}
private void unbindPreview(@NonNull ProcessCameraProvider cameraProvider) {
_analysis = null;
cameraProvider.unbindAll();
}
@Override
public void onQrCodeDetected(Result result) {
if (_analysis != null) {
try {
Uri uri = Uri.parse(result.getText().trim());
if (uri.getScheme() != null && uri.getScheme().equals(GoogleAuthInfo.SCHEME_EXPORT)) {
handleExportUri(uri);
} else {
handleUri(uri);
}
} catch (GoogleAuthInfoException e) {
e.printStackTrace();
unbindPreview(_cameraProvider);
Dialogs.showErrorDialog(this,
e.isPhoneFactor() ? R.string.read_qr_error_phonefactor : R.string.read_qr_error,
e, ((dialog, which) -> bindPreview(_cameraProvider)));
}
}
}
@@ -159,7 +214,7 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
finish(_entries);
}
Toast.makeText(this, getString(R.string.google_qr_export_scanned, _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
Toast.makeText(this, getResources().getQuantityString(R.plurals.google_qr_export_scanned, export.getBatchSize(), _batchIndex + 1, export.getBatchSize()), Toast.LENGTH_SHORT).show();
} else if (_batchIndex != batchIndex) {
Toast.makeText(this, getString(R.string.google_qr_export_unexpected, _batchIndex + 1, batchIndex + 1), Toast.LENGTH_SHORT).show();
}
@@ -171,42 +226,4 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
setResult(RESULT_OK, intent);
finish();
}
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
if (dual) {
switch (_facing) {
case CAMERA_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CAMERA_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
}
item.setVisible(dual);
}
}
private static int getCameraId(int facing) {
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
Camera.getCameraInfo(i, info);
if (info.facing == facing) {
return i;
}
}
return -1;
}
private static int getRearCameraId() {
return getCameraId(CAMERA_FACING_BACK);
}
private static int getFrontCameraId() {
return getCameraId(CAMERA_FACING_FRONT);
}
}
@@ -1,136 +0,0 @@
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;
}
}
@@ -17,6 +17,7 @@ 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.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@@ -39,6 +40,7 @@ public class SlotManagerActivity extends AegisActivity implements SlotAdapter.Li
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_slots);
setSupportActionBar(findViewById(R.id.toolbar));
_edited = false;
ActionBar bar = getSupportActionBar();
@@ -0,0 +1,145 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import java.util.ArrayList;
import java.util.List;
public class TransferEntriesActivity extends AegisActivity {
private List<GoogleAuthInfo> _authInfos;
private ImageView _qrImage;
private TextView _issuer;
private TextView _accountName;
private TextView _entriesCount;
private Button _nextButton;
private Button _previousButton;
private VaultManager _vault;
private int _currentEntryCount = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share_entry);
setSupportActionBar(findViewById(R.id.toolbar));
_vault = getApp().getVaultManager();
_qrImage = findViewById(R.id.ivQrCode);
_issuer = findViewById(R.id.tvIssuer);
_accountName = findViewById(R.id.tvAccountName);
_entriesCount = findViewById(R.id.tvEntriesCount);
_nextButton = findViewById(R.id.btnNext);
_previousButton = findViewById(R.id.btnPrevious);
if (getSupportActionBar() != null){
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Intent intent = getIntent();
_authInfos = (ArrayList<GoogleAuthInfo>) intent.getSerializableExtra("authInfos");
int controlVisibility = _authInfos.size() != 1 ? View.VISIBLE : View.INVISIBLE;
_nextButton.setVisibility(controlVisibility);
_nextButton.setOnClickListener(v -> {
if (_currentEntryCount < _authInfos.size()) {
_previousButton.setVisibility(View.VISIBLE);
_currentEntryCount++;
generateQR();
if (_currentEntryCount == _authInfos.size()) {
_nextButton.setText(R.string.done);
}
} else {
finish();
}
});
_previousButton.setOnClickListener(v -> {
if (_currentEntryCount > 1 ) {
_nextButton.setText(R.string.next);
_currentEntryCount--;
generateQR();
if (_currentEntryCount == 1) {
_previousButton.setVisibility(View.INVISIBLE);
}
}
});
generateQR();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
private void generateQR() {
GoogleAuthInfo selectedEntry = _authInfos.get(_currentEntryCount - 1);
_issuer.setText(selectedEntry.getIssuer());
_accountName.setText(selectedEntry.getAccountName());
_entriesCount.setText(getResources().getQuantityString(R.plurals.entries_count, _authInfos.size(), _currentEntryCount, _authInfos.size()));
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix;
try {
bitMatrix = writer.encode(selectedEntry.getUri().toString(), BarcodeFormat.QR_CODE, 512, 512);
} catch (WriterException e) {
Dialogs.showErrorDialog(this, R.string.unable_to_generate_qrcode, e);
return;
}
@ColorInt int backgroundColor = Color.WHITE;
if (getConfiguredTheme() == Theme.LIGHT) {
TypedValue typedValue = new TypedValue();
getTheme().resolveAttribute(R.attr.background, typedValue, true);
backgroundColor = typedValue.data;
}
int width = bitMatrix.getWidth();
int height = bitMatrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
_qrImage.setImageBitmap(bitmap);
}
}
@@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.content.Context;
import com.beemdevelopment.aegis.R;
public class ChangelogDialog extends SimpleWebViewDialog {
private ChangelogDialog() {
super(R.string.changelog);
}
public static ChangelogDialog create() {
return new ChangelogDialog();
}
@Override
protected String getContent(Context context) {
String content = readAssetAsString(context, "changelog.html");
return String.format(content, getBackgroundColor(), getTextColor());
}
}
@@ -1,4 +1,4 @@
package com.beemdevelopment.aegis.ui;
package com.beemdevelopment.aegis.ui.dialogs;
import android.app.Activity;
import android.app.Dialog;
@@ -6,34 +6,47 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.provider.Settings;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.activity.ComponentActivity;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.crypto.Cipher;
@@ -53,28 +66,25 @@ public class Dialogs {
dialog.show();
}
public static void showDeleteEntryDialog(Activity activity, DialogInterface.OnClickListener onDelete) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.delete_entry))
.setMessage(activity.getString(R.string.delete_entry_description))
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
}
public static void showDeleteEntriesDialog(Activity activity, List<String> services, DialogInterface.OnClickListener onDelete) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_delete_entry, null);
TextView textMessage = view.findViewById(R.id.text_message);
TextView textExplanation = view.findViewById(R.id.text_explanation);
textExplanation.setText(activity.getString(R.string.delete_entry_explanation, TextUtils.join(", ", services)));
public static void showDeleteEntriesDialog(Activity activity, DialogInterface.OnClickListener onDelete, int totalEntries) {
String title, message;
if (totalEntries > 1) {
if (services.size() > 1) {
title = activity.getString(R.string.delete_entries);
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
message = activity.getResources().getQuantityString(R.plurals.delete_entries_description, services.size(), services.size());
} else {
title = activity.getString(R.string.delete_entry);
message = activity.getString(R.string.delete_entry_description);
}
textMessage.setText(message);
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setView(view)
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
@@ -89,10 +99,14 @@ public class Dialogs {
.create());
}
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
public static void showSetPasswordDialog(ComponentActivity activity, Dialogs.SlotListener listener) {
Zxcvbn zxcvbn = new Zxcvbn();
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);
ProgressBar barPasswordStrength = view.findViewById(R.id.progressBar);
TextView textPasswordStrength = view.findViewById(R.id.text_password_strength);
TextInputLayout textPasswordWrapper = view.findViewById(R.id.text_password_wrapper);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
@@ -113,6 +127,7 @@ public class Dialogs {
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
@@ -140,19 +155,30 @@ public class Dialogs {
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new KeyDerivationTask.Params(slot, password));
KeyDerivationTask.Params params = new KeyDerivationTask.Params(slot, password);
task.execute(activity.getLifecycle(), params);
});
});
TextWatcher watcher = new TextWatcher() {
@Override
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal);
Strength strength = zxcvbn.measure(textPassword.getText());
barPasswordStrength.setProgress(strength.getScore());
barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(PasswordStrengthHelper.getColor(strength.getScore()))));
textPasswordStrength.setText((textPassword.getText().length() != 0) ? PasswordStrengthHelper.getString(strength.getScore(), activity) : "");
textPasswordWrapper.setError(strength.getFeedback().getWarning());
strength.wipe();
}
@Override
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable c) {
}
};
@@ -162,13 +188,14 @@ public class Dialogs {
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int hintId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
TextInputEditText input = view.findViewById(R.id.text_input);
TextInputLayout inputLayout = view.findViewById(R.id.text_input_layout);
if (isSecret) {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
input.setHint(hintId);
inputLayout.setHint(hintId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
@@ -177,16 +204,23 @@ public class Dialogs {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
});
if (cancelListener != null) {
builder.setOnCancelListener(cancelListener);
}
if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(true);
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
showTextInputDialog(context, titleId, 0, hintId, listener, isSecret);
showTextInputDialog(context, titleId, 0, hintId, listener, null, isSecret);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
@@ -197,14 +231,57 @@ public class Dialogs {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, R.string.set_password, 0, R.string.password, listener, cancelListener, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, true);
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, null, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, R.string.set_password, messageId, R.string.password, listener, cancelListener, true);
}
public static void showPasswordInputDialog(Context context, @StringRes int titleId, @StringRes int messageId, TextInputListener listener, DialogInterface.OnCancelListener cancelListener) {
showTextInputDialog(context, titleId, messageId, R.string.password, listener, cancelListener, true);
}
public static void showCheckboxDialog(Context context, @StringRes int titleId, @StringRes int messageId, @StringRes int checkboxMessageId, CheckboxInputListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_checkbox, null);
CheckBox checkBox = view.findViewById(R.id.checkbox);
checkBox.setText(checkboxMessageId);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setNegativeButton(R.string.no, (dialog1, which) ->
listener.onCheckboxInputResult(false))
.setPositiveButton(R.string.yes, (dialog1, which) ->
listener.onCheckboxInputResult(checkBox.isChecked()));
if (messageId != 0) {
builder.setMessage(messageId);
}
AlertDialog dialog = builder.create();
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setEnabled(false);
buttonOK.set(button);
});
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> buttonOK.get().setEnabled(isChecked));
showSecureDialog(dialog);
}
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.setMinValue(1);
numberPicker.setMaxValue(60);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
numberPicker.setWrapSelectorWheel(true);
@@ -266,6 +343,7 @@ public class Dialogs {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.error_occurred)
.setView(view)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
if (listener != null) {
listener.onClick(dialog1, which);
@@ -320,6 +398,45 @@ public class Dialogs {
.create());
}
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
List<String> names = importers.stream().map(DatabaseImporter.Definition::getName).collect(Collectors.toList());
int i = 0;
if (!isDirect) {
i = names.indexOf(context.getString(R.string.app_name));
}
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
TextView helpText = view.findViewById(R.id.text_importer_help);
setImporterHelpText(helpText, importers.get(i), isDirect);
ListView listView = view.findViewById(R.id.list_importers);
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
listView.setItemChecked(i, true);
listView.setOnItemClickListener((parent, view1, position, id) -> {
setImporterHelpText(helpText, importers.get(position), isDirect);
});
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_application)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition()));
})
.create());
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
} else {
view.setText(definition.getHelp());
}
}
public interface CheckboxInputListener {
void onCheckboxInputResult(boolean checkbox);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
@@ -332,4 +449,8 @@ public class Dialogs {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
}
@@ -0,0 +1,162 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.GridLayoutManager;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.ui.glide.IconLoader;
import com.beemdevelopment.aegis.ui.views.IconAdapter;
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
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 com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.textfield.TextInputEditText;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class IconPickerDialog {
private IconPickerDialog() {
}
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
BottomSheetDialog dialog = new BottomSheetDialog(activity);
dialog.setContentView(view);
dialog.create();
FrameLayout rootView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
rootView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
IconAdapter adapter = new IconAdapter(dialog.getContext(), issuer, new IconAdapter.Listener() {
@Override
public void onIconSelected(IconPack.Icon icon) {
dialog.dismiss();
listener.onIconSelected(icon);
}
@Override
public void onCustomSelected() {
dialog.dismiss();
listener.onCustomSelected();
}
});
class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
@NonNull
@Override
public List<IconPack.Icon> getPreloadItems(int position) {
IconPack.Icon icon = adapter.getIconAt(position);
return Collections.singletonList(icon);
}
@Nullable
@Override
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
return Glide.with(dialog.getContext())
.asDrawable()
.load(icon.getFile())
.set(IconLoader.ICON_TYPE, icon.getIconType())
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(false);
}
}
TextInputEditText iconSearch = view.findViewById(R.id.text_search_icon);
iconSearch.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(rootView);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
iconSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String query = s.toString();
adapter.setQuery(query.isEmpty() ? null : query);
}
@Override
public void afterTextChanged(Editable s) {
}
});
ViewPreloadSizeProvider<IconPack.Icon> preloadSizeProvider = new ViewPreloadSizeProvider<>();
IconPreloadProvider modelProvider = new IconPreloadProvider();
RecyclerViewPreloader<IconPack.Icon> preloader = new RecyclerViewPreloader<>(activity, modelProvider, preloadSizeProvider, 10);
IconRecyclerView recyclerView = view.findViewById(R.id.list_icons);
GridLayoutManager layoutManager = recyclerView.getGridLayoutManager();
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (adapter.getItemViewType(position) == R.layout.card_icon) {
return 1;
}
return recyclerView.getSpanCount();
}
});
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
recyclerView.addOnScrollListener(preloader);
adapter.loadIcons(iconPacks.get(0));
textIconPack.setText(iconPacks.get(0).getName());
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
List<String> iconPackNames = iconPacks.stream()
.map(IconPack::getName)
.collect(Collectors.toList());
PopupMenu popupMenu = new PopupMenu(activity, v);
popupMenu.setOnMenuItemClickListener(item -> {
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
adapter.loadIcons(pack);
String query = iconSearch.getText().toString();
if (!query.isEmpty()) {
adapter.setQuery(query);
}
textIconPack.setText(pack.getName());
return true;
});
Menu menu = popupMenu.getMenu();
for (String name : iconPackNames) {
menu.add(name);
}
popupMenu.show();
});
return dialog;
}
}
@@ -0,0 +1,22 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.content.Context;
import com.beemdevelopment.aegis.R;
public class LicenseDialog extends SimpleWebViewDialog {
private LicenseDialog() {
super(R.string.license);
}
public static LicenseDialog create() {
return new LicenseDialog();
}
@Override
protected String getContent(Context context) {
String license = readAssetAsString(context, "LICENSE");
String html = readAssetAsString(context, "license.html");
return String.format(html, license, getBackgroundColor(), getTextColor());
}
}
@@ -0,0 +1,93 @@
package com.beemdevelopment.aegis.ui.dialogs;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
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.annotation.StringRes;
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 com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public abstract class SimpleWebViewDialog extends DialogFragment {
private Theme _theme;
private final @StringRes int _title;
protected SimpleWebViewDialog(@StringRes int title) {
_title = title;
}
protected abstract String getContent(Context context);
@SuppressLint("InflateParams")
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view;
try {
view = 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(_title)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.show();
String content = getContent(getContext());
final WebView webView = view.findViewById(R.id.web_view);
webView.loadData(content, "text/html", "UTF-8");
return dialog;
}
public SimpleWebViewDialog setTheme(Theme theme) {
_theme = theme;
return this;
}
protected String getBackgroundColor() {
int backgroundColorResource = _theme == Theme.AMOLED ? R.attr.cardBackgroundFocused : R.attr.cardBackground;
return colorToCSS(ThemeHelper.getThemeColor(backgroundColorResource, getContext().getTheme()));
}
protected String getTextColor() {
return colorToCSS(0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getContext().getTheme()));
}
@SuppressLint("DefaultLocale")
private static String colorToCSS(int color) {
return String.format("rgb(%d, %d, %d)", Color.red(color), Color.green(color), Color.blue(color));
}
protected static String readAssetAsString(Context context, String name) {
try (InputStream inStream = context.getAssets().open(name);
InputStreamReader reader = new InputStreamReader(inStream, StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,132 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.GroupManagerActivity;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.ArrayList;
import java.util.HashSet;
public class AppearancePreferencesFragment extends PreferencesFragment {
private Preference _groupsPreference;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_appearance);
Preferences prefs = getPreferences();
_groupsPreference = findPreference("pref_groups");
_groupsPreference.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getActivity(), GroupManagerActivity.class);
intent.putExtra("groups", new ArrayList<>(getVault().getGroups()));
startActivityForResult(intent, CODE_GROUPS);
return true;
});
int currentTheme = prefs.getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme]));
darkModePreference.setOnPreferenceClickListener(preference -> {
int currentTheme1 = prefs.getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
prefs.setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
getResult().putExtra("needsRecreate", true);
getActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference langPreference = findPreference("pref_lang");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
} else {
// Setting locale doesn't work on Marshmallow or below
langPreference.setVisible(false);
}
int currentViewMode = prefs.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(preference -> {
int currentViewMode1 = prefs.getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
prefs.setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
getResult().putExtra("needsRefresh", true);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
});
Preference codeDigitGroupingPreference = findPreference("pref_code_group_size");
codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference issuerPreference = findPreference("pref_account_name");
issuerPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null && requestCode == CODE_GROUPS) {
onGroupManagerResult(resultCode, data);
}
}
private void onGroupManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
HashSet<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (VaultEntry entry : getVault().getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
saveVault();
}
}
@@ -0,0 +1,139 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManagerException;
public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference;
private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference;
@Override
public void onResume() {
super.onResume();
updateBackupPreference();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_backups);
Preferences prefs = getPreferences();
_backupsPreference = findPreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) {
selectBackupsLocation();
} else {
prefs.setIsBackupsEnabled(false);
updateBackupPreference();
}
return false;
});
_androidBackupsPreference = findPreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
prefs.setIsAndroidBackupsEnabled((boolean) newValue);
updateBackupPreference();
getVault().androidBackupDataChanged();
return false;
});
Uri backupLocation = prefs.getBackupsLocation();
_backupsLocationPreference = findPreference("pref_backups_location");
if (backupLocation != null) {
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
_backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation();
return false;
});
_backupsTriggerPreference = findPreference("pref_backups_trigger");
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (prefs.isBackupsEnabled()) {
try {
getVault().backup();
Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.backup_error, e);
}
}
return true;
});
_backupsVersionsPreference = findPreference("pref_backups_versions");
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
number = number * 5 + 5;
prefs.setBackupsVersionCount(number);
_backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount()));
});
return false;
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_BACKUPS) {
onSelectBackupsLocationResult(resultCode, data);
}
}
private void onSelectBackupsLocationResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags);
Preferences prefs = getPreferences();
prefs.setBackupsLocation(uri);
prefs.setIsBackupsEnabled(true);
prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference();
}
private void updateBackupPreference() {
boolean encrypted = getVault().isEncryptionEnabled();
boolean androidBackupEnabled = getPreferences().isAndroidBackupsEnabled() && encrypted;
boolean backupEnabled = getPreferences().isBackupsEnabled() && encrypted;
_androidBackupsPreference.setChecked(androidBackupEnabled);
_androidBackupsPreference.setEnabled(encrypted);
_backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted);
_backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled);
}
private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_BACKUPS);
}
}
@@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.os.Bundle;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.R;
public class BehaviorPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_behavior);
Preference copyOnTapPreference = findPreference("pref_copy_on_tap");
copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
}
}
@@ -0,0 +1,160 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.icons.IconPack;
import com.beemdevelopment.aegis.icons.IconPackException;
import com.beemdevelopment.aegis.icons.IconPackExistsException;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
private static final int CODE_IMPORT = 0;
private IconPackAdapter _adapter;
private IconPackManager _iconPackManager;
private View _iconPacksView;
private RecyclerView _iconPacksRecyclerView;
private LinearLayout _noIconPacksView;
private FabScrollHelper _fabScrollHelper;
public IconPacksManagerFragment() {
super(R.layout.fragment_icon_packs);
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
_iconPackManager = ((AegisApplication) getContext().getApplicationContext()).getIconPackManager();
FloatingActionButton fab = view.findViewById(R.id.fab);
fab.setOnClickListener(v -> startImportIconPack());
_fabScrollHelper = new FabScrollHelper(fab);
_noIconPacksView = view.findViewById(R.id.vEmptyList);
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
_iconPacksView = view.findViewById(R.id.view_icon_packs);
_adapter = new IconPackAdapter(this);
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
_iconPacksRecyclerView.setLayoutManager(layoutManager);
_iconPacksRecyclerView.setAdapter(_adapter);
_iconPacksRecyclerView.setNestedScrollingEnabled(false);
_iconPacksRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
_fabScrollHelper.onScroll(dx, dy);
}
});
for (IconPack pack : _iconPackManager.getIconPacks()) {
_adapter.addIconPack(pack);
}
updateEmptyState();
}
@Override
public void onRemoveIconPack(IconPack pack) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.remove_icon_pack)
.setMessage(R.string.remove_icon_pack_description)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return;
}
_adapter.removeIconPack(pack);
updateEmptyState();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
importIconPack(data.getData());
}
}
private void importIconPack(Uri uri) {
ImportIconPackTask task = new ImportIconPackTask(getContext(), result -> {
Exception e = result.getException();
if (e instanceof IconPackExistsException) {
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
.setTitle(R.string.error_occurred)
.setMessage(R.string.icon_pack_import_exists_error)
.setPositiveButton(R.string.yes, (dialog, which) -> {
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
importIconPack(uri);
}
})
.setNegativeButton(R.string.no, null)
.create());
} else if (e != null) {
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_import_error, e);
} else {
_adapter.addIconPack(result.getIconPack());
updateEmptyState();
}
});
task.execute(getLifecycle(), new ImportIconPackTask.Params(_iconPackManager, uri));
}
private boolean removeIconPack(IconPack pack) {
try {
_iconPackManager.removeIconPack(pack);
} catch (IconPackException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
return false;
}
_adapter.removeIconPack(pack);
updateEmptyState();
return true;
}
private void startImportIconPack() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/zip");
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_IMPORT);
}
private void updateEmptyState() {
if (_adapter.getItemCount() > 0) {
_iconPacksView.setVisibility(View.VISIBLE);
_noIconPacksView.setVisibility(View.GONE);
} else {
_iconPacksView.setVisibility(View.GONE);
_noIconPacksView.setVisibility(View.VISIBLE);
}
}
}
@@ -0,0 +1,359 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import androidx.preference.Preference;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.DropdownHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.AegisActivity;
import com.beemdevelopment.aegis.ui.ImportEntriesActivity;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.ExportTask;
import com.beemdevelopment.aegis.ui.tasks.ImportFileTask;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.crypto.Cipher;
public class ImportExportPreferencesFragment extends PreferencesFragment {
// keep a reference to the type of database converter that was selected
private Class<? extends DatabaseImporter> _importerType;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_import_export);
if (savedInstanceState != null) {
_importerType = (Class<? extends DatabaseImporter>) savedInstanceState.getSerializable("importerType");
}
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showImportersDialog(getContext(), false, definition -> {
_importerType = definition.getType();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
AegisActivity.Helper.startExtActivityForResult(this, intent, CODE_IMPORT_SELECT);
});
return true;
});
Preference importAppPreference = findPreference("pref_import_app");
importAppPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showImportersDialog(getContext(), true, definition -> {
startImportEntriesActivity(definition.getType(), null);
});
return true;
});
Preference exportPreference = findPreference("pref_export");
exportPreference.setOnPreferenceClickListener(preference -> {
startExport();
return true;
});
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("importerType", _importerType);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CODE_IMPORT) {
getResult().putExtra("needsRecreate", true);
} else if (data != null) {
switch (requestCode) {
case CODE_IMPORT_SELECT:
onImportSelectResult(resultCode, data);
break;
case CODE_EXPORT:
// intentional fallthrough
case CODE_EXPORT_PLAIN:
// intentional fallthrough
case CODE_EXPORT_GOOGLE_URI:
onExportResult(requestCode, resultCode, data);
break;
}
}
}
private void onImportSelectResult(int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
ImportFileTask.Params params = new ImportFileTask.Params(uri, "import", null);
ImportFileTask task = new ImportFileTask(getContext(), result -> {
if (result.getException() == null) {
startImportEntriesActivity(_importerType, result.getFile());
} else {
Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, result.getException());
}
});
task.execute(getLifecycle(), params);
}
private void startImportEntriesActivity(Class<? extends DatabaseImporter> importerType, File file) {
Intent intent = new Intent(getActivity(), ImportEntriesActivity.class);
intent.putExtra("importerType", importerType);
intent.putExtra("file", file);
startActivityForResult(intent, CODE_IMPORT);
}
private void startExport() {
View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null);
TextView warningText = view.findViewById(R.id.text_export_warning);
CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt);
CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept);
AutoCompleteTextView dropdown = view.findViewById(R.id.dropdown_export_format);
DropdownHelper.fillDropdown(getContext(), dropdown, R.array.export_formats);
dropdown.setText(getString(R.string.export_format_aegis), false);
dropdown.setOnItemClickListener((parent, view1, position, id) -> {
checkBoxEncrypt.setChecked(position == 0);
checkBoxEncrypt.setEnabled(position == 0);
warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE);
});
AlertDialog dialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.pref_export_summary)
.setView(view)
.setNeutralButton(R.string.share, null)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.setOnShowListener(d -> {
Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> {
warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE);
checkBoxAccept.setChecked(false);
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
btnPos.setEnabled(isChecked);
btnNeutral.setEnabled(isChecked);
});
btnPos.setOnClickListener(v -> {
dialog.dismiss();
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_TITLE, fileInfo.toString());
AegisActivity.Helper.startExtActivityForResult(this, intent, requestCode);
});
btnNeutral.setOnClickListener(v -> {
dialog.dismiss();
int pos = getStringResourceIndex(R.array.export_formats, dropdown.getText().toString());
if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) {
return;
}
File file;
try {
VaultBackupManager.FileInfo fileInfo = getExportFileInfo(pos, checkBoxEncrypt.isChecked());
file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir());
} catch (IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
int requestCode = getExportRequestCode(pos, checkBoxEncrypt.isChecked());
startExportVault(requestCode, cb -> {
try (OutputStream stream = new FileOutputStream(file)) {
cb.exportVault(stream);
} catch (IOException | VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
return;
}
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file);
Intent intent = new Intent(Intent.ACTION_SEND)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(getExportMimeType(requestCode))
.putExtra(Intent.EXTRA_STREAM, uri);
Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary));
AegisActivity.Helper.startExtActivity(this, chooser);
});
});
});
Dialogs.showSecureDialog(dialog);
}
private static int getExportRequestCode(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN;
}
return CODE_EXPORT_GOOGLE_URI;
}
private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) {
if (spinnerPos == 0) {
String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN;
return new VaultBackupManager.FileInfo(filename);
}
return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt");
}
private static String getExportMimeType(int requestCode) {
return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json";
}
private File getExportCacheDir() throws IOException {
File dir = new File(getContext().getCacheDir(), "export");
if (!dir.exists() && !dir.mkdir()) {
throw new IOException(String.format("Unable to create directory %s", dir));
}
return dir;
}
private void startExportVault(int requestCode, StartExportCallback cb) {
switch (requestCode) {
case CODE_EXPORT:
if (getVault().isEncryptionEnabled()) {
cb.exportVault(stream -> getVault().export(stream));
} else {
Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
} catch (SlotException e) {
onException(e);
return;
}
cb.exportVault(stream -> getVault().export(stream, creds));
}
@Override
public void onException(Exception e) {
}
});
}
break;
case CODE_EXPORT_PLAIN:
cb.exportVault((stream) -> getVault().export(stream, null));
break;
case CODE_EXPORT_GOOGLE_URI:
cb.exportVault((stream) -> getVault().exportGoogleUris(stream));
break;
}
}
private void onExportResult(int requestCode, int resultCode, Intent data) {
Uri uri = data.getData();
if (resultCode != Activity.RESULT_OK || uri == null) {
return;
}
startExportVault(requestCode, cb -> {
File file;
OutputStream outStream = null;
try {
file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir());
outStream = new FileOutputStream(file);
cb.exportVault(outStream);
new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri));
} catch (VaultManagerException | IOException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} finally {
try {
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
private int getStringResourceIndex(@ArrayRes int id, String string) {
String[] res = getResources().getStringArray(id);
for (int i = 0; i < res.length; i++) {
if (res[i].equalsIgnoreCase(string)) {
return i;
}
}
return -1;
}
private class ExportResultListener implements ExportTask.Callback {
@Override
public void onTaskFinished(Exception e) {
if (e != null) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e);
} else {
Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
}
}
private interface FinishExportCallback {
void exportVault(OutputStream stream) throws IOException, VaultManagerException;
}
private interface StartExportCallback {
void exportVault(FinishExportCallback exportCb);
}
}
@@ -0,0 +1,13 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.os.Bundle;
import com.beemdevelopment.aegis.R;
public class MainPreferencesFragment extends PreferencesFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences);
}
}
@@ -0,0 +1,88 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.CallSuper;
import androidx.preference.PreferenceFragmentCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
public abstract class PreferencesFragment extends PreferenceFragmentCompat {
// activity request codes
public static final int CODE_IMPORT_SELECT = 0;
public static final int CODE_SLOTS = 2;
public static final int CODE_GROUPS = 3;
public static final int CODE_IMPORT = 4;
public static final int CODE_EXPORT = 5;
public static final int CODE_EXPORT_PLAIN = 6;
public static final int CODE_EXPORT_GOOGLE_URI = 7;
public static final int CODE_BACKUPS = 8;
private AegisApplication _app;
private Intent _result;
private Preferences _prefs;
private VaultManager _vault;
@Override
@CallSuper
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
_app = (AegisApplication) getActivity().getApplication();
_prefs = _app.getPreferences();
_vault = _app.getVaultManager();
setResult(new Intent());
}
@Override
@CallSuper
public void onResume() {
super.onResume();
Intent intent = getActivity().getIntent();
String preference = intent.getStringExtra("pref");
if (preference != null) {
scrollToPreference(preference);
intent.removeExtra("pref");
}
}
public Intent getResult() {
return _result;
}
public void setResult(Intent result) {
_result = result;
getActivity().setResult(Activity.RESULT_OK, _result);
}
protected AegisApplication getApp() {
return _app;
}
protected Preferences getPreferences() {
return _prefs;
}
protected VaultManager getVault() {
return _vault;
}
protected boolean saveVault() {
try {
_vault.save(true);
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.saving_error, e);
return false;
}
return true;
}
}
@@ -0,0 +1,412 @@
package com.beemdevelopment.aegis.ui.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.SlotManagerActivity;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
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 java.util.List;
import javax.crypto.Cipher;
import static android.text.TextUtils.isDigitsOnly;
public class SecurityPreferencesFragment extends PreferencesFragment {
private SwitchPreference _encryptionPreference;
private SwitchPreference _biometricsPreference;
private Preference _autoLockPreference;
private Preference _setPasswordPreference;
private Preference _slotsPreference;
private Preference _passwordReminderPreference;
private SwitchPreferenceCompat _pinKeyboardPreference;
@Override
public void onResume() {
super.onResume();
updateEncryptionPreferences();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_security);
Preference tapToRevealPreference = findPreference("pref_tap_to_reveal");
tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRefresh", true);
return true;
});
Preference screenPreference = findPreference("pref_secure_screen");
screenPreference.setOnPreferenceChangeListener((preference, newValue) -> {
getResult().putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean) newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
return true;
});
Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time");
tapToRevealTimePreference.setSummary(getPreferences().getTapToRevealTime() + " seconds");
tapToRevealTimePreference.setOnPreferenceClickListener(preference -> {
Dialogs.showNumberPickerDialog(getActivity(), number -> {
getPreferences().setTapToRevealTime(number);
tapToRevealTimePreference.setSummary(number + " seconds");
getResult().putExtra("needsRefresh", true);
});
return false;
});
_encryptionPreference = findPreference("pref_encryption");
_encryptionPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!getVault().isEncryptionEnabled()) {
Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener());
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.disable_encryption)
.setMessage(getText(R.string.disable_encryption_description))
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
try {
getVault().disableEncryption();
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e);
return;
}
// clear the KeyStore
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.clear();
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
getPreferences().setIsBackupsEnabled(false);
getPreferences().setIsAndroidBackupsEnabled(false);
updateEncryptionPreferences();
})
.setNegativeButton(android.R.string.no, null)
.create());
}
return false;
});
_biometricsPreference = findPreference("pref_biometrics");
_biometricsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
VaultFileCredentials creds = getVault().getCredentials();
SlotList slots = creds.getSlots();
if (!slots.has(BiometricSlot.class)) {
if (BiometricsHelper.isAvailable(getContext())) {
BiometricSlotInitializer initializer = new BiometricSlotInitializer(SecurityPreferencesFragment.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 biometric slot
BiometricSlot slot = slots.find(BiometricSlot.class);
slots.remove(slot);
getVault().setCredentials(creds);
// remove the KeyStore key
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.deleteKey(slot.getUUID().toString());
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
saveVault();
updateEncryptionPreferences();
}
return false;
});
_setPasswordPreference = findPreference("pref_password");
_setPasswordPreference.setOnPreferenceClickListener(preference -> {
Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener());
return false;
});
_slotsPreference = findPreference("pref_slots");
_slotsPreference.setOnPreferenceClickListener(preference -> {
Intent intent = new Intent(getActivity(), SlotManagerActivity.class);
intent.putExtra("creds", getVault().getCredentials());
startActivityForResult(intent, CODE_SLOTS);
return true;
});
_pinKeyboardPreference = findPreference("pref_pin_keyboard");
_pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if (!(boolean) newValue) {
return true;
}
Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> {
if (isDigitsOnly(new String(password))) {
List<PasswordSlot> slots = getVault().getCredentials().getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener());
task.execute(getLifecycle(), params);
} else {
_pinKeyboardPreference.setChecked(false);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.pin_keyboard_error_description)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
}
}, dialog -> {
_pinKeyboardPreference.setChecked(false);
});
return false;
});
_autoLockPreference = findPreference("pref_auto_lock");
_autoLockPreference.setSummary(getAutoLockSummary());
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
final boolean[] checkedItems = new boolean[items.length];
for (int i = 0; i < items.length; i++) {
checkedItems[i] = getPreferences().isAutoLockTypeEnabled(items[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_auto_lock_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int autoLock = Preferences.AUTO_LOCK_OFF;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
autoLock |= items[i];
}
}
getPreferences().setAutoLockMask(autoLock);
_autoLockPreference.setSummary(getAutoLockSummary());
})
.setNegativeButton(android.R.string.cancel, null);
Dialogs.showSecureDialog(builder.create());
return false;
});
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (data != null && requestCode == CODE_SLOTS) {
onSlotManagerResult(resultCode, data);
}
}
private void onSlotManagerResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
getVault().setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
private void updateEncryptionPreferences() {
boolean encrypted = getVault().isEncryptionEnabled();
_encryptionPreference.setChecked(encrypted, true);
_setPasswordPreference.setVisible(encrypted);
_biometricsPreference.setVisible(encrypted);
_slotsPreference.setEnabled(encrypted);
_autoLockPreference.setVisible(encrypted);
_pinKeyboardPreference.setVisible(encrypted);
if (encrypted) {
SlotList slots = getVault().getCredentials().getSlots();
boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1;
boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1;
boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio;
boolean canUseBio = BiometricsHelper.isAvailable(getContext());
_setPasswordPreference.setEnabled(!multiPassword);
_biometricsPreference.setEnabled(canUseBio && !multiBio);
_biometricsPreference.setChecked(slots.has(BiometricSlot.class), true);
_slotsPreference.setVisible(showSlots);
_passwordReminderPreference.setVisible(slots.has(BiometricSlot.class));
} else {
_setPasswordPreference.setEnabled(false);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
_passwordReminderPreference.setVisible(false);
}
}
private String getAutoLockSummary() {
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < settings.length; i++) {
if (getPreferences().isAutoLockTypeEnabled(settings[i])) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(descriptions[i].toLowerCase());
}
}
if (builder.length() == 0) {
return getString(R.string.pref_auto_lock_summary_disabled);
}
return getString(R.string.pref_auto_lock_summary, builder.toString());
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = getVault().getCredentials();
SlotList slots = creds.getSlots();
try {
// encrypt the master key for this slot
slot.setKey(creds.getKey(), cipher);
// remove the old master password slot
PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class);
if (oldSlot != null) {
slots.remove(oldSlot);
}
// add the new master password slot
slots.add(slot);
} catch (SlotException e) {
onException(e);
return;
}
getVault().setCredentials(creds);
saveVault();
if (getPreferences().isPinKeyboardEnabled()) {
_pinKeyboardPreference.setChecked(false);
Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onException(Exception e) {
e.printStackTrace();
updateEncryptionPreferences();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener {
@Override
public void onInitializeSlot(BiometricSlot slot, Cipher cipher) {
VaultFileCredentials creds = getVault().getCredentials();
try {
slot.setKey(creds.getKey(), cipher);
} catch (SlotException e) {
e.printStackTrace();
onSlotInitializationFailed(0, e.toString());
return;
}
creds.getSlots().add(slot);
getVault().setCredentials(creds);
saveVault();
updateEncryptionPreferences();
}
@Override
public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) {
if (!BiometricsHelper.isCanceled(errorCode)) {
Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString);
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
VaultFileCredentials creds = new VaultFileCredentials();
try {
slot.setKey(creds.getKey(), cipher);
creds.getSlots().add(slot);
getVault().enableEncryption(creds);
} catch (VaultManagerException | SlotException e) {
onException(e);
return;
}
getActivity().startService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
@Override
public void onException(Exception e) {
e.printStackTrace();
Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e);
}
}
private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback {
@Override
public void onTaskFinished(PasswordSlotDecryptTask.Result result) {
if (result != null) {
_pinKeyboardPreference.setChecked(true);
} else {
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.pin_keyboard_error)
.setMessage(R.string.invalid_password)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, null)
.create());
_pinKeyboardPreference.setChecked(false);
}
}
}
}
@@ -1,6 +1,7 @@
package com.beemdevelopment.aegis.ui.glide;
import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import androidx.annotation.NonNull;
@@ -9,7 +10,9 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import java.nio.ByteBuffer;
@GlideModule
@@ -17,5 +20,8 @@ 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());
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder())
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
}
}
@@ -2,9 +2,11 @@ package com.beemdevelopment.aegis.ui.glide;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.ModelLoader;
@@ -14,6 +16,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.nio.ByteBuffer;
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
@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));
@@ -25,7 +29,7 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
}
public static class Fetcher implements DataFetcher<ByteBuffer> {
private VaultEntry _model;
private final VaultEntry _model;
private Fetcher(VaultEntry model) {
_model = model;

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