Compare commits

...
417 Commits
Author SHA1 Message Date
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 bed8c9172d Release v1.2-beta4 2020-05-30 13:52:20 +02:00
Michael SchättgenandGitHub ef1ac3e5e9 Merge pull request #441 from alexbakker/fix-lock-crash
Fix crash that would occur when locking the screen with a locked vault
2020-05-30 13:06:25 +02: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
Michael SchättgenandGitHub 95180ba963 Merge pull request #444 from alexbakker/fix-export-backup
Fix an issue where backups where enabled when exporting the vault
2020-05-28 10:01:19 +02:00
Alexander Bakker 56d7f0ef7b Fix an issue where backups where enabled when exporting the vault
This is really stupid.
2020-05-28 09:54:29 +02:00
Alexander Bakker fbd6a5931e Fix crash that would occur when locking the screen with a locked vault 2020-05-26 11:33:35 +02:00
Alexander Bakker 66dd3561cf Release v1.2-beta3 2020-05-25 17:32:32 +02:00
Michael SchättgenandGitHub 58db3fdaa2 Merge pull request #438 from alexbakker/fix-intro-creds
Fix issue where VaultManager was erroneously initialized with creds in intro
2020-05-25 16:41:35 +02:00
Alexander Bakker 92b6738ff4 Fix issue where VaultManager was erroneously initialized with creds in intro
Also fixes another issue where previously, if a user made it to the last intro
slide and then navigated back to change the security options, any changes would
be ignored.
2020-05-25 16:29:14 +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
Alexander Bakker 6a2992530e Release v1.2-beta2 2020-05-16 21:36:52 +02:00
Alexander Bakker 1f839a886d Don't replace the Bouncy Castle security provider
We only use Bouncy Castle for scrypt, so replacing the security provider was a
nice to have. It's causing issues because Proguard removes Bouncy Castle
classes. As we just released a beta, this is a quick fix and we may revisit this
later.
2020-05-16 21:32:13 +02:00
Alexander Bakker 91f9a6756a Release v1.2-beta1 2020-05-16 13:53:30 +02:00
Alexander BakkerandGitHub 89137c0873 Merge pull request #417 from michaelschattgen/feature/update-translations
Update translations
2020-05-16 13:44:42 +02:00
Michael Schättgen ffa31c7aa3 Update translations 2020-05-16 13:37:41 +02:00
Michael SchättgenandGitHub e12bfdbe58 Merge pull request #365 from alexbakker/spongy-to-bouncy
Replace SpongyCastle with BouncyCastle
2020-05-16 12:50:58 +02:00
Alexander Bakker 3c887d8392 Replace SpongyCastle with BouncyCastle
SpongyCastle is a fork of BouncyCastle. We originally used this fork to 1) have
access to scrypt and 2) prevent a package name collision with the bundled
BouncyCastle. We don't actually need to use the fork anymore, because the
package name of the bundled BouncyCastle was changed in Android. SpongyCastle
has also gotten quite outdated in recent years.

The built-in version of BouncyCastle is replaced with the one bundled with the
app at runtime, so that we have a recent version even on older Android versions.

This also updates Gradle and the Gradle Android plugin, to fix a build error I
was running into when I added the dependency to BouncyCastle.
2020-05-16 12:30:38 +02:00
Michael SchättgenandGitHub b4d2b6b113 Merge pull request #410 from alexbakker/fix-0-byte-export
Reuse existing PreferencesFragment when restoring from instance state
2020-05-16 12:27:52 +02:00
Michael SchättgenandGitHub fe40c4b952 Merge pull request #407 from alexbakker/auth-plus-zip4j
Import from ZIP for the Authenticator Plus, instead of SQLite databases
2020-05-16 12:27:27 +02:00
Alexander BakkerandGitHub 9319c74932 Merge pull request #403 from michaelschattgen/feature/card-entry-overhaul
Add minor card entry layout overhaul
2020-05-16 12:20:46 +02:00
Alexander BakkerandGitHub 4c60b04057 Merge pull request #412 from michaelschattgen/feature/increase-reminder-period
Increase password reminder period to 30 days
2020-05-13 09:17:46 +02:00
Michael Schättgen 1a24f62074 Increase password reminder period to 30 days 2020-05-12 22:23:47 +02:00
Alexander Bakker 1824d38f75 Import from ZIP for the Authenticator Plus, instead of SQLite databases 2020-05-12 12:09:46 +02:00
Michael SchättgenandGitHub e41f200bcd Merge pull request #406 from alexbakker/import-google-qr-export
Add initial support for importing Google Authenticator export QR codes
2020-05-12 11:50:31 +02:00
Alexander Bakker ccc990f854 Reuse existing PreferencesFragment when restoring from instance state
This fixes an issue where the existing PreferencesFragment was discarded when
restoring PreferencesActivity from instance state. This issue caused the
PreferencesFragment to no longer receive activity results. Most notably, this
resulted in 0 byte vault files being created when exporting the vault.
2020-05-11 17:15:54 +02:00
Alexander Bakker 56bde0e19b Add support for importing from the new Google Authenticator export QR codes 2020-05-11 14:02:59 +02:00
Michael SchättgenandGitHub 6b650e777f Merge pull request #391 from alexbakker/time-sync-warning
Display a warning if automatic time sync is not enabled
2020-05-09 18:33:41 +02:00
Alexander Bakker 49a7fda932 Display a warning if automatic time sync is not enabled
This'll display a warning to users who don't have automatic time synchronization
enabled on their device. Aegis will try to take the user to the right settings
menu if they tap "Yes". Users also have the option to silence the warning.

[<img width=300 src="https://alexbakker.me/u/jf1o8087lr.png">](https://alexbakker.me/u/jf1o8087lr.png)
2020-05-09 18:14:10 +02:00
Michael SchättgenandGitHub 8ff817856e Merge pull request #378 from alexbakker/better-errors
Improve overall exception handling and error feedback to the user
2020-05-09 17:16:33 +02:00
Michael Schättgen 3a85a05078 Add minor card entry layout overhaul 2020-05-09 16:55:27 +02:00
Alexander Bakker cda78c56c5 Improve overall exception handling and error feedback to the user
The main goals of this patch are to:
- Improve the exception handling in Aegis and the way we present errors messages
  to the user when they occur.
- Write exception stack traces to the log in more places, so that the ADB logs
  we ask for from our users when reporting bugs become more useful.
- Reduce the amount of times we throw a RuntimeException, particularly when an
  Android Keystore operation fails.

Achieving the above goals ended up resulting in a very large refactor. The
intro and unlock flow of the app need to be retested entirely.
2020-05-09 16:12:38 +02:00
Michael SchättgenandGitHub 499f930086 Merge pull request #400 from NotWoods/about-activity
Change license link color so it is legible on dark
2020-05-09 14:53:54 +02:00
Michael SchättgenandGitHub 393b815409 Merge pull request #394 from alexbakker/update-appintro
Update AppIntro to 6.0.0
2020-05-09 14:42:23 +02:00
Michael SchättgenandGitHub 984010dc7a Merge pull request #392 from alexbakker/export-filename
Append a timestamp to the filename of exported vaults
2020-05-09 14:39:18 +02:00
Michael SchättgenandGitHub 0bbc413423 Merge pull request #389 from alexbakker/edit-entry-fix
Write entries to the vault directly in EditEntryActivity
2020-05-09 14:11:59 +02:00
Michael SchättgenandGitHub 98a90e3e1e Merge pull request #388 from alexbakker/proguard
Minify and optimize the APK using ProGuard
2020-05-09 14:08:47 +02:00
Tiger Oakes 3ccbece822 Change license link color so it is legible on dark 2020-05-06 17:48:48 -07:00
Alexander Bakker 0f81233d4f Add the Hungarian translation to the settings menu 2020-05-06 15:28:15 +02:00
Alexander BakkerandGitHub f6b1f02d11 Merge pull request #395 from meskobalazs/master
Add Hungarian translation
2020-05-06 15:26:13 +02:00
Alexander Bakker 301476ff5c Write entries to the vault directly in EditEntryActivity
This makes it so that EditEntryActivity directly saves entries to the vault,
instead of passing them back to MainActivity through an Intent first. This
prevents crashes that can occur when an entry has a large icon and the Bundle
inside the Intent becomes too large.

This is the first part of a series of patches I plan on submitting, where I try
to repair the damage done by my misguided obsession of only touching the global
state in certain places.
2020-05-06 15:07:19 +02:00
Balázs Meskó b84f44bf1e Add Hungarian metadata 2020-05-06 15:00:54 +02:00
Balázs Meskó 842afe6745 Add Hungarian translation 2020-05-06 11:01:46 +02:00
Alexander Bakker bc5fa68de8 Update AppIntro to 6.0.0 2020-05-05 19:14:10 +02:00
Alexander Bakker fec2f3ff94 Append a timestamp to the filename of exported vaults
Close #297
2020-05-03 18:30:59 +02:00
Alexander Bakker 77718809ef Minify and optimize the APK using ProGuard
This enables some minification and optimization options to shrink the size of
our APK. A release APK would previously be 12 MB in size, but would now be 8.2
MB.

To test, check if *all* of the functionality of the app still works,
particularly parts that refer to dependencies. You'll know if ProGuard broke
something when the app crashes with a ``ClassNotFoundException`` or similar
exception. I think I've covered all of the cases where ProGuard removed too much
in the rule file.

Also, I was curious why our APK had gotten so large to begin with. I did some
digging and found that this is caused by the SQLCipher dependency. The APK
shrinks down to 2.7 MB without it! We should consider whether having support for
importing from Authenticator Plus is worth the extra 5.5 MB in size.
2020-05-02 15:28:38 +02:00
Alexander BakkerandGitHub 9b328893f7 Merge pull request #381 from michaelschattgen/feature/period-improvement
Use most frequent period to show progress
2020-05-01 17:17:39 +02:00
Michael SchättgenandGitHub 45378b8128 Merge pull request #386 from beemdevelopment/funding
Create FUNDING.yml
2020-05-01 10:56:08 +02:00
Alexander BakkerandGitHub b830c6db1b Create FUNDING.yml 2020-05-01 09:40:42 +02:00
Michael Schättgen 7773a51f2a Use most frequent period to show progress 2020-04-27 02:29:10 +02:00
Michael SchättgenandGitHub f68badb659 Merge pull request #377 from alexbakker/fix-weird-reminder-crash
Show the password reminder popup after the window has been attached
2020-04-26 23:33:10 +02:00
Michael SchättgenandGitHub 4ea8501fa9 Merge pull request #369 from Timbals/master
automatically adapt to system theme
2020-04-26 19:59:39 +02:00
Michael SchättgenandGitHub 1f86697d2d Merge pull request #380 from alexbakker/password-popup
Allow password reminder popup dismissal by typing
2020-04-26 19:59:19 +02:00
Alexander Bakker faf24e15bd Allow password reminder popup dismissal by typing
Close #379
2020-04-26 11:12:25 +02:00
Alexander BakkerandGitHub 592d482a9b Merge pull request #370 from maximbaz/maximbaz-patch-1
Show account name only if it's set
2020-04-25 14:18:31 +02:00
Maxim BazandMaxim Baz ad05e375f1 Show account name only if it's set 2020-04-25 12:39:06 +02:00
Alexander Bakker 8b30d451a4 Show the password reminder popup after the window has been attached
I noticed a strange crash in the Play Console:

```
android.view.WindowManager$BadTokenException:
  at android.view.ViewRootImpl.setView (ViewRootImpl.java:828)
  at android.view.WindowManagerGlobal.addView (WindowManagerGlobal.jav>
  at android.view.WindowManagerImpl.addView (WindowManagerImpl.java:93)
  at android.widget.PopupWindow.invokePopup (PopupWindow.java:1434)
  at android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1284)
  at android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1240)
  at android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1219)
  at com.beemdevelopment.aegis.ui.AuthActivity.lambda$showPasswordRemi>
  at com.beemdevelopment.aegis.ui.-$$Lambda$AuthActivity$WWHxRKllBPcyH>
  at android.os.Handler.handleCallback (Handler.java:873)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:220)
  at android.app.ActivityThread.main (ActivityThread.java:6929)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (Runt>
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:870)
```

I can't reproduce this on my device, and we haven't received any reports from
users, but it would be pretty bad if Aegis crashes on some devices when showing
the password reminder popup. This patch is an attempt to fix that (see:
https://stackoverflow.com/a/33809860/12972657).
2020-04-25 12:21:34 +02:00
Alexander BakkerandGitHub abe78370ea Merge pull request #375 from l-marchesi/master
Option to change code display format (Issue #373)
2020-04-24 12:49:10 +02:00
Lukas Marchesi 00e2e90aa7 Add setting to change from 3 digit group size to 2 digit group size 2020-04-23 23:19:38 +02:00
Tim Balsfulland f37d70b58e automatically adapt to system theme
added two new theme options:
SYSTEM: dynamically switches between light and dark
SYSTEM_AMOLED: dynamically switches between light and amoled

reversed workaround for amoled themed preferences
launch screen now always follows the system theme
2020-04-20 15:29:41 +02:00
Alexander BakkerandGitHub 09065c705b Merge pull request #368 from michaelschattgen/feature/copy-multiselect
Fix bug where multiselect triggers copy animation
2020-04-19 17:00:00 +02:00
Michael Schättgen ad58960831 Fix bug where multiselect triggers copy animation
Fix multiselect
2020-04-19 14:18:57 +02:00
orangenbaumblattandGitHub 7ce74d378f Added selection indicator in icon slot (#344)
Changed selection color for black theme


Changed indicator color to black secondary


Fix indicator flickering when scrolling


Applied patch


Fix unsharp selection icon


Add selection indicators to small and compact view
2020-04-18 18:54:37 +02:00
Michael SchättgenandGitHub 8c0ba2dd1b Merge pull request #366 from alexbakker/steam-otpauth
Add support for otpauth://steam URI's
2020-04-18 14:06:36 +02:00
Michael SchättgenandGitHub 016c64610e Merge pull request #359 from alexbakker/othauth-importer
Add support for importing a plain text Google Authenticator URI file
2020-04-18 14:05:35 +02:00
Michael SchättgenandGitHub c578543e18 Merge pull request #341 from alexbakker/backups
Add support for backups
2020-04-18 13:44:44 +02:00
Alexander BakkerandGitHub d247750e62 Merge pull request #323 from michaelschattgen/feature-copied
Improve method to notify users on copy
2020-04-18 13:42:12 +02:00
Michael SchättgenandGitHub b8896ec1df Merge pull request #358 from alexbakker/fix-353
Rename "profile" text to "entry"
2020-04-18 13:40:00 +02:00
Alexander Bakker 4a69e9efb4 Add support for backups
Allow users to select a folder where automatic backups will be created. This
also bumps minSdkVersion to 21.
2020-04-18 13:38:25 +02:00
Michael Schättgen b43bac37db Improve method to notify users on copy
Add minor code improvements
2020-04-18 13:17:08 +02:00
Alexander Bakker 0c7b41747e Add support for otpauth://steam URI's
Close #343
2020-04-18 12:22:57 +02:00
Alexander Bakker 8fda6937a5 Add support for importing a plain text Google Authenticator URI file
This is the "standard" format discussed in #138.
2020-04-11 19:14:09 +02:00
Alexander Bakker 0a4a933a2a Rename "profile" text to "entry" 2020-04-11 14:37:28 +02:00
VstoryandGitHub 866466d158 Update Chinese translation (#337)
* Update Chinese translation

* Remove extra characters
2020-03-20 19:20:40 +01:00
Alexander BakkerandGitHub d89d5eac9c Merge pull request #338 from orangenbaumblatt/master
Keep groups when importing from Authenticator Plus
2020-03-20 16:23:53 +01:00
orangenbaumblatt 6885463b11 Keep groups when importing from Authenticator Plus 2020-03-18 21:16:43 +01:00
Alexander BakkerandGitHub a273fdaa1d Merge pull request #326 from andreysaksonov/master
Update Russian localization for new strings
2020-03-18 16:22:58 +01:00
Alexander BakkerandGitHub ec60e98b2e Merge pull request #329 from orangenbaumblatt/master
Added Database Import from Authenticator Plus
2020-03-18 14:48:19 +01:00
orangenbaumblatt dc0f7c4668 Added Database Import from Authenticator Plus
Added copyright notice


Rearrange item position in import dialog


Refactor SqlImportHelper to support SQLCipher


Improved stream handling
2020-03-12 18:34:17 +01:00
Andrey Saksonov ba7fe15a57 update Russian localization 2020-03-12 18:04:30 +03:00
Alexander BakkerandGitHub 3d51b84188 Merge pull request #325 from michaelschattgen/fix-confirm-password
Improve confirm password functionality
2020-03-04 10:25:29 +01:00
Alexander BakkerandGitHub f66cebb727 Merge pull request #324 from michaelschattgen/fix-require-confirmation
Set confirmation not required on BiometricPrompt
2020-03-04 10:05:55 +01:00
Andrey Saksonov 9e355c8e52 update Russian localization 2020-03-04 10:39:12 +03:00
Andrey Saksonov 9ce0982191 update Russian localization 2020-03-04 10:16:06 +03:00
Andrey Saksonov f543802f9e Merge remote-tracking branch 'upstream/master' 2020-03-04 09:53:43 +03:00
Andrey Saksonov 32aecadf75 update Russian localization 2020-03-04 09:52:55 +03:00
Michael Schättgen d39e3d7b90 Improve confirm password functionality 2020-03-03 23:43:33 +01:00
Michael Schättgen bd66659b92 Set confirmation not required on BiometricPrompt 2020-03-03 22:14:53 +01:00
Alexander Bakker 1a7a794dc9 Bump versionCode to 29
The APK released to the Play Store has versionCode set to 29 instead of 28,
because the update had to be resubmitted due to an erroneous rejection by
Google.
2020-02-29 10:27:31 +01:00
Michael SchättgenandGitHub 2519633265 Merge pull request #318 from alexbakker/fix-bna
Make the parsing logic of the QR code URI more robust
2020-02-24 21:31:37 +01:00
Michael SchättgenandGitHub e38121efee Merge pull request #306 from alexbakker/atomic-file
Protect writes of the vault file against corruption with AtomicFile
2020-02-24 21:31:21 +01:00
Alexander BakkerandGitHub 261b036f75 Merge pull request #319 from h4waii/patch-1
Change Capitalization
2020-02-24 08:27:48 +01:00
h4waiiandGitHub 7e857b53a7 Update strings.xml 2020-02-24 07:50:01 +08:00
Michael SchättgenandGitHub 73e1d72403 Merge pull request #305 from alexbakker/fix-authy
Add support for importing encrypted and manually entered tokens from Authy
2020-02-21 22:24:23 +01:00
Alexander Bakker 5731cccddc Fix build 2020-02-21 17:23:40 +01:00
Michael SchättgenandGitHub 48d22fc06d Merge pull request #304 from alexbakker/microsoft-auth
Add support for importing from Microsoft Authenticator
2020-02-20 18:43:57 +01:00
Michael SchättgenandGitHub 964ed294a5 Merge pull request #303 from alexbakker/totp-authenticator
Add support for importing from TOTP Authenticator
2020-02-20 17:46:39 +01:00
Alexander Bakker 2a4bb902df Add support for importing encrypted and manually entered tokens from Authy 2020-02-19 12:02:17 +01:00
Alexander Bakker d3463b82bc Add support for importing from Microsoft Authenticator
Adds support for importing TOTP and Microsoft accounts from Microsoft
Authenticator. Microsoft accounts also use TOTP, but with 8 digits instead
of 6.

This implementation suffers from the same bug (#82) as Google Authenticator, but I
haven't thought of a nice way to solve it yet. We should fix that before
including this feature in a release, though.
2020-02-19 12:00:41 +01:00
Alexander Bakker ef536a0c07 Abstract away the SQLite database reading logic into a separate class 2020-02-19 12:00:01 +01:00
Alexander Bakker 7e7c5317b1 Add support for importing from TOTP Authenticator
This is by far the most ridiculous format/'security' I've seen so far.
2020-02-19 11:59:34 +01:00
Alexander Bakker 2bf3546971 Make the parsing logic of the QR code URI more robust 2020-02-19 11:17:16 +01:00
Michael SchättgenandGitHub cd42b0dc65 Merge pull request #271 from alexbakker/guava
Replace implementations of Base16, Base32 and Base64 with Guava
2020-02-17 22:23:43 +01:00
Alexander Bakker 4ba3caeaf4 Protect writes of the vault file against corruption with AtomicFile
This adds a recovery mechanism for (probably extremely rare) cases where the app
may be killed before it is finished writing the vault file to disk. In the
example below, we see that AtomicFile moved ``aegis.json`` to ``aegis.json.bak``
before writing to ``aegis.json``.

```
bonito:/ # ls -lah /data/data/com.beemdevelopment.aegis.debug/files
total 27M
drwxrwx--x 2 u0_a306 u0_a306 3.4K 2020-02-02 13:22 .
drwx------ 6 u0_a306 u0_a306 3.4K 2020-02-01 19:51 ..
-rw------- 1 u0_a306 u0_a306  19M 2020-02-02 13:22 aegis.json
-rw------- 1 u0_a306 u0_a306  34M 2020-02-02 13:21 aegis.json.bak
```

Because the app was killed before it could
finish writing, it is only 19M in size, instead of the expected 34M. The next
time the app starts, AtomicFile will notice that the .bak file is still present,
and use that instead of the corrupted ``aegis.json`` file.
2020-02-02 13:23:45 +01:00
Alexander Bakker 10ac1af6b0 Replace implementations of Base16, Base32 and Base64 with Guava
I kept the classes in the encoding package and turned them into wrappers for
Guava. I also changed the functions in the Base32 class to take and return
strings insteads if character arrays.
2020-02-01 14:11:55 +01:00
Alexander BakkerandGitHub 833e75ade1 Merge pull request #301 from acolombo25/master
Add navigation bar color to themes
2020-01-29 20:23:48 +01:00
Andrea Colombo 7bbd80ed21 Add navigation bar color to themes 2020-01-27 11:34:34 +01:00
Alexander Bakker dbedc9d1d3 Release v1.1.4 2020-01-22 20:42:22 +01:00
Michael SchättgenandGitHub 3bdcc25ebe Merge pull request #298 from alexbakker/saf-json
Append ".json" extension to export filenames
2020-01-22 00:40:03 -08:00
Alexander Bakker d0108e9859 Append ".json" extension to export filenames
Apparently not all SAF providers append a file extension based on the specified
MIME type.
2020-01-21 09:34:06 +01:00
Alexander Bakker 025c89d78c Release v1.1.3 2020-01-20 21:36:15 +01:00
Alexander BakkerandGitHub c52e60b410 Merge pull request #288 from michaelschattgen/set-locale
Fix setting locale on 7.0
2020-01-20 21:35:04 +01:00
Michael Schättgen 3cc644193f Update languages and gitignore 2020-01-20 21:34:20 +01:00
Michael Schättgen 8020792024 Fix setting locale on 7.0 2020-01-20 21:01:17 +01:00
Michael SchättgenandGitHub 837a27f9d7 Merge pull request #293 from alexbakker/password-reminder
Remind users who use biometrics to enter their password periodically
2020-01-20 11:56:49 -08:00
Michael SchättgenandGitHub 5c71f6254b Merge pull request #296 from alexbakker/fix-295
Fix #295
2020-01-20 11:56:32 -08:00
Alexander Bakker bbea6c2ae4 Fix #295 2020-01-19 23:00:03 +01:00
Alexander Bakker fa799e9542 Remind users who use biometrics to enter their password periodically
Instead of showing the reminder after x unlocks, I decided to show the reminder
2 weeks after the vault was last unlocked with the password. Let me know if you
agree with that.

![](https://alexbakker.me/u/115z6be7go.png)
2020-01-19 15:53:04 +01:00
364 changed files with 23213 additions and 5759 deletions
+1
View File
@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/beemdevelopment']
+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
+1
View File
@@ -39,3 +39,4 @@ captures/
# Keystore files
*.jks
crowdin.properties
-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
+134 -41
View File
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'com.google.protobuf'
def getCmdOutput = { cmd ->
def stdout = new ByteArrayOutputStream()
@@ -12,17 +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"
minSdkVersion 19
targetSdkVersion 29
versionCode 26
versionName "1.1.2"
applicationId "${packageName}"
minSdkVersion 21
targetSdkVersion 30
versionCode 47
versionName "2.0-beta2"
multiDexEnabled true
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "GIT_BRANCH", "\"${getGitBranch()}\""
testInstrumentationRunner "com.beemdevelopment.aegis.AegisTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
lintOptions {
@@ -31,65 +40,149 @@ 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 {
minifyEnabled false
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
removeUnusedResources true
obfuscate false
optimizeCode false
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
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
removeUnusedResources true
obfuscate false
optimizeCode true
proguardFiles getDefaultProguardFile('proguard-defaults.txt'), 'proguard-rules.pro'
}
}
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
coreLibraryDesugaringEnabled true
}
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.8.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
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.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.getbase:floatingactionbutton:1.10.1'
implementation 'com.github.apl-devs:appintro:5.1.0'
implementation 'com.github.avito-tech:krop:0.44'
implementation 'com.madgag.spongycastle:core:1.58.0.0'
implementation '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 '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'
}
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
</lint>
+8
View File
@@ -15,3 +15,11 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# 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 { *; }
-keep class !org.bouncycastle.jce.provider.X509LDAPCertStoreSpi { *; }
@@ -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,188 @@
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);
/*changeFilter(_groupName);
changeFilter(R.string.filter_ungrouped);
changeFilter(R.string.all);*/
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 changeFilter(String text) {
openContextualActionModeOverflowMenu();
onView(withText(R.string.filter)).perform(click());
onView(withText(text)).perform(click());
}
private void changeFilter(@IdRes int resId) {
changeFilter(ApplicationProvider.getApplicationContext().getString(resId));
}*/
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 profile" />
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
+144
View File
@@ -31,6 +31,150 @@
</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>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>
<li>Add navigation bar color to themes</li>
<li>Add support for importing from TOTP Authenticator</li>
<li>Add support for importing from Microsoft Authenticator</li>
<li>Add support for importing from Authenticator Plus</li>
<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>
<li>Automatically adapt to system theme</li>
<li>Add setting to change from 3 digit group size to 2 digit group size</li>
<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>
<h4>Fixes</h4>
<ul>
<li>The export filename was missing the ".json" extension in some cases</li>
</ul>
<h3>Version 1.1.3</h3>
<h4>New</h4>
<ul>
<li>Password reminder for users who use biometric unlock</li>
</ul>
<h4>Fixes</h4>
<ul>
<li>Tokens would not refresh in some rare cases</li>
</ul>
<h3>Version 1.1.2</h3>
<h4>New</h4>
<ul>
+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,32 +12,52 @@ 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;
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;
import java.util.List;
public class AegisApplication extends Application {
private VaultFile _vaultFile;
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();
_manager = new VaultManager(this);
_prefs = new Preferences(this);
_lockListeners = new ArrayList<>();
_iconPackManager = new IconPackManager(this);
Iconics.init(this);
Iconics.registerFont(new MaterialDesignIconic());
@@ -49,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();
}
@@ -58,16 +84,61 @@ public class AegisApplication extends Application {
}
}
public boolean isVaultLocked() {
return _manager == null;
}
/**
* Loads the vault file from disk at the default location, stores an internal
* reference to it for future use and returns it. This must only be called before
* initVaultManager() or after lock().
*/
public VaultFile loadVaultFile() throws VaultManagerException {
if (!isVaultLocked()) {
throw new AssertionError("loadVaultFile() may only be called before initVaultManager() or after lock()");
}
if (_vaultFile == null) {
_vaultFile = VaultManager.readVaultFile(this);
}
return _vaultFile;
}
/**
* Initializes the vault manager by decrypting the given vaultFile with the given
* creds. This removes the internal reference to the raw vault file.
*/
public VaultManager initVaultManager(VaultFile vaultFile, VaultFileCredentials creds) throws VaultManagerException {
_vaultFile = null;
_manager = VaultManager.init(this, vaultFile, creds);
return _manager;
}
/**
* Initializes the vault manager with the given vault and creds. This removes the
* internal reference to the raw vault file.
*/
public VaultManager initVaultManager(Vault vault, VaultFileCredentials creds) {
_vaultFile = null;
_manager = new VaultManager(this, vault, creds);
return _manager;
}
public VaultManager getVaultManager() {
return _manager;
}
public IconPackManager getIconPackManager() {
return _iconPackManager;
}
public Preferences getPreferences() {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && _manager.isLoaded() && _manager.isEncryptionEnabled() && !_manager.isLocked();
public boolean isAutoLockEnabled(int autoLockType) {
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
}
public void registerLockListener(LockListener listener) {
@@ -78,10 +149,25 @@ public class AegisApplication extends Application {
_lockListeners.remove(listener);
}
public void lock() {
_manager.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));
@@ -96,11 +182,12 @@ 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")
.setShortLabel(getString(R.string.new_profile))
.setLongLabel(getString(R.string.add_new_profile))
.setShortLabel(getString(R.string.new_entry))
.setLongLabel(getString(R.string.add_new_entry))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
@@ -122,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
}
@@ -3,45 +3,106 @@ package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import java.util.Date;
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) {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isSearchAccountNameEnabled() {
return _prefs.getBoolean("pref_search_names", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean 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);
}
public boolean isPasswordReminderEnabled() {
return _prefs.getBoolean("pref_password_reminder", true);
}
public boolean isPasswordReminderNeeded() {
long diff = new Date().getTime() - getPasswordReminderTimestamp().getTime();
long days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
return isPasswordReminderEnabled() && days >= 30;
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
public void resetPasswordReminderTimestamp() {
_prefs.edit().putLong("pref_password_reminder_counter", new Date().getTime()).apply();
}
public boolean isAccountNameVisible() {
return _prefs.getBoolean("pref_account_name", true);
}
public int getCodeGroupSize() {
if (_prefs.getBoolean("pref_code_group_size", false)) {
return 2;
} else {
return 3;
}
}
public boolean isIntroDone() {
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) {
@@ -65,7 +126,7 @@ public class Preferences {
}
public Theme getCurrentTheme() {
return Theme.fromInteger(_prefs.getInt("pref_current_theme", 0));
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
}
public void setCurrentTheme(Theme theme) {
@@ -95,6 +156,72 @@ 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() {
return _prefs.getBoolean("pref_backups", false);
}
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
return Uri.parse(str);
}
return null;
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
public int getBackupsVersionCount() {
return _prefs.getInt("pref_backups_versions", 5);
}
public void setBackupsVersionCount(int versions) {
_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);
}
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);
}
}
@@ -3,7 +3,9 @@ package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED;
AMOLED,
SYSTEM,
SYSTEM_AMOLED;
private static Theme[] _values;
@@ -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
);
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.encoding.HexException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -30,7 +30,7 @@ public class CryptParameters implements Serializable {
return obj;
}
public static CryptParameters fromJson(JSONObject obj) throws JSONException, HexException {
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
byte[] nonce = Hex.decode(obj.getString("nonce"));
byte[] tag = Hex.decode(obj.getString("tag"));
return new CryptParameters(nonce, tag);
@@ -2,7 +2,7 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import org.spongycastle.crypto.generators.SCrypt;
import org.bouncycastle.crypto.generators.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -2,9 +2,10 @@ package com.beemdevelopment.aegis.crypto;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -12,6 +13,7 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.ProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
@@ -21,8 +23,6 @@ import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import androidx.annotation.RequiresApi;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String STORE_NAME = "AndroidKeyStore";
@@ -61,6 +61,14 @@ public class KeyStoreHandle {
.build());
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new KeyStoreHandleException(e);
}
@@ -88,18 +96,14 @@ public class KeyStoreHandle {
@RequiresApi(api = Build.VERSION_CODES.M)
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher
// and see if KeyPermanentlyInvalidatedException is thrown
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
// apparently KitKat doesn't like KeyPermanentlyInvalidatedException, even when guarded with a version check
// it will throw a java.lang.VerifyError when its listed in a 'catch' statement
// so instead, check for it here
if (e instanceof KeyPermanentlyInvalidatedException) {
return true;
}
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
@@ -17,7 +17,7 @@ public class MasterKey implements Serializable {
public MasterKey(SecretKey key) {
if (key == null) {
throw new NullPointerException();
throw new IllegalArgumentException("Key cannot be null");
}
_key = key;
}
@@ -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},};
}
}
@@ -1,155 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// modified for use in Aegis
import com.google.common.io.BaseEncoding;
/* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info.
*
* As modified by Patrick Woodworth:
*
* Copyright 2011 Patrick Woodworth
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
/**
* Base32 - encodes and decodes RFC3548 Base32
* (see http://www.faqs.org/rfcs/rfc3548.html )
*
* @author Robert Kaye
* @author Gordon Mohr
*/
public class Base32 {
private static final String base32Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static final int[] base32Lookup =
{ 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_'
0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g'
0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'
0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w'
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
};
private Base32() {
/**
* Encodes byte array to Base32 String.
*
* @param bytes Bytes to encode.
* @return Encoded byte array <code>bytes</code> as a String.
*
*/
public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte;
char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
/* Is the current digit going to span a byte boundary? */
if (index > 3) {
if ((i + 1) < bytes.length) {
nextByte =
(bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256);
} else {
nextByte = 0;
}
digit = currByte & (0xFF >> index);
index = (index + 5) % 8;
digit <<= index;
digit |= nextByte >> (8 - index);
i++;
} else {
digit = (currByte >> (8 - (index + 5))) & 0x1F;
index = (index + 5) % 8;
if (index == 0)
i++;
}
base32[j++] = base32Chars.charAt(digit);
}
return Arrays.copyOf(base32, j);
}
/**
* Decodes the given Base32 String to a raw byte array.
*
* @param base32
* @return Decoded <code>base32</code> String as a raw byte array.
*/
public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
// stop decoding when a padding char is encountered
if (base32[i] == '=') {
// make sure the rest is also padding, but don't bother verifying the length
for (int j = i + 1; j < base32.length; j++) {
if (base32[j] != '=') {
throw new Base32Exception("bad padding");
}
}
break;
}
lookup = base32[i] - '0';
digit = decodeDigit(lookup);
if (index <= 3) {
index = (index + 5) % 8;
if (index == 0) {
bytes[offset] |= digit;
offset++;
if (offset >= bytes.length)
break;
} else {
bytes[offset] |= digit << (8 - index);
}
} else {
index = (index + 5) % 8;
bytes[offset] |= (digit >>> index);
offset++;
if (offset >= bytes.length) {
break;
}
bytes[offset] |= digit << (8 - index);
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return bytes;
}
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base32Exception extends Exception {
public Base32Exception(String message) {
super(message);
}
}
@@ -1,24 +1,27 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class Base64 {
private static final int _flags = android.util.Base64.NO_WRAP;
private Base64() {
}
public static byte[] decode(String s) throws Base64Exception {
public static byte[] decode(String s) throws EncodingException {
try {
return android.util.Base64.decode(s, _flags);
return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException e) {
throw new Base64Exception(e);
throw new EncodingException(e);
}
}
public static byte[] decode(byte[] s) throws EncodingException {
return decode(new String(s, StandardCharsets.UTF_8));
}
public static String encode(byte[] data) {
byte[] encoded = android.util.Base64.encode(data, _flags);
return new String(encoded, StandardCharsets.UTF_8);
return BaseEncoding.base64().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class Base64Exception extends Exception {
public Base64Exception(Throwable cause) {
super(cause);
}
}
@@ -0,0 +1,9 @@
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
}
@@ -1,46 +1,21 @@
package com.beemdevelopment.aegis.encoding;
// The hexadecimal utility functions in this file were taken and modified from: http://www.docjar.com/html/api/com/sun/xml/internal/bind/DatatypeConverterImpl.java.html
// It is licensed under GPLv2 with a classpath exception.
import com.google.common.io.BaseEncoding;
public class Hex {
private Hex() {
}
private static int hexToBin(char ch) {
if ('0' <= ch && ch <= '9') return ch - '0';
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
return -1;
}
private static final char[] hexCode = "0123456789abcdef".toCharArray();
public static byte[] decode(String s) throws HexException {
final int len = s.length();
if (len % 2 != 0)
throw new HexException("hexBinary needs to be even-length: " + s);
byte[] out = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int h = hexToBin(s.charAt(i));
int l = hexToBin(s.charAt(i + 1));
if (h == -1 || l == -1)
throw new HexException("contains illegal character for hexBinary: " + s);
out[i / 2] = (byte) (h * 16 + l);
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
return out;
}
public static String encode(byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(hexCode[(b >> 4) & 0xF]);
r.append(hexCode[(b & 0xF)]);
}
return r.toString();
return BaseEncoding.base16().lowerCase().encode(data);
}
}
@@ -1,7 +0,0 @@
package com.beemdevelopment.aegis.encoding;
public class HexException extends Exception {
public HexException(String message) {
super(message);
}
}
@@ -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 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.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.otp.OtpInfoException;
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 | Base64Exception 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.Base32Exception;
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());
}
@@ -201,7 +228,7 @@ public class AndOtpImporter extends DatabaseImporter {
String type = obj.getString("type").toLowerCase();
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
@@ -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,18 +248,85 @@ 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);
} catch (DatabaseImporterException | Base32Exception | OtpInfoException | JSONException e) {
} catch (DatabaseImporterException | EncodingException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
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;
}
}
}
@@ -0,0 +1,77 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
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;
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";
public AuthenticatorPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
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 {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
}
@@ -1,15 +1,23 @@
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.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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.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;
@@ -18,52 +26,194 @@ 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;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.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;
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
public AuthyImporter(Context context) {
super(context);
}
@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;
}
}
return new State(array);
return read(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private JSONArray _obj;
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);
}
private State(JSONArray obj) {
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;
private EncryptedState(JSONArray array) {
super(true);
_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 {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false);
_obj = obj;
_array = array;
}
@Override
@@ -71,8 +221,8 @@ public class AuthyImporter extends DatabaseImporter {
Result result = new Result();
try {
for (int i = 0; i < _obj.length(); i++) {
JSONObject entryObj = _obj.getJSONObject(i);
for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
@@ -90,42 +240,60 @@ public class AuthyImporter extends DatabaseImporter {
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = entry.getString("originalName");
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").toCharArray());
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 | Base32Exception e) {
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info) {
String seperator = "";
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
if (!isAuthy) {
String separator = "";
if (info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
seperator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
seperator = " - ";
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 + seperator, "");
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}
private static class AuthyEntryInfo {
String OriginalName;
String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
@@ -3,44 +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.ByteInputStream;
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("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
_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) {
@@ -51,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 {
@@ -75,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 {
@@ -132,40 +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 {
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
return stream.getBytes();
}
}
public InputStream getStream() {
return _stream;
}
/**
* Reports whether this reader reads the internal state of an app.
* @return true if reading from internal file, false if reading from external file
*/
public boolean isInternal() {
return _internal;
}
}
public interface DecryptListener {
void onStateDecrypted(State state);
void onError(Exception e);
public static abstract class DecryptListener {
protected abstract void onStateDecrypted(State state);
protected abstract void onError(Exception e);
protected abstract void onCanceled();
}
}
@@ -3,6 +3,11 @@ package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(String message, String text) {
super(message);
_text = text;
}
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
@@ -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,27 +1,21 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.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.topjohnwu.superuser.ShellUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.io.InputStream;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
@@ -34,48 +28,23 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
@Override
protected String getAppPkgName() {
return _pkgName;
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
protected String getAppSubPath() {
return _subPath;
}
@Override
public State read(FileReader reader) throws DatabaseImporterException {
File file;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("google-import-", "", getContext().getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
ShellUtils.pump(reader.getStream(), out);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY, null)) {
try (Cursor cursor = db.rawQuery("SELECT * FROM accounts", null)) {
List<Entry> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
Entry entry = new Entry(cursor);
entries.add(entry);
} while(cursor.moveToNext());
}
return new State(entries);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
} finally {
// always delete the temporary file
file.delete();
}
public 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);
}
public static class State extends DatabaseImporter.State {
@@ -104,7 +73,7 @@ public class GoogleAuthImporter extends DatabaseImporter {
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(entry.getSecret().toCharArray());
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
@@ -125,33 +94,13 @@ public class GoogleAuthImporter extends DatabaseImporter {
}
return new VaultEntry(info, name, entry.getIssuer());
} catch (Base32Exception | OtpInfoException | DatabaseImporterException e) {
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static String getString(Cursor cursor, String columnName) {
return getString(cursor, columnName, null);
}
private static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
private static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
private static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
private static class Entry {
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _email;
@@ -159,11 +108,12 @@ public class GoogleAuthImporter extends DatabaseImporter {
private long _counter;
public Entry(Cursor cursor) {
_type = getInt(cursor, "type");
_secret = getString(cursor, "secret");
_email = getString(cursor, "email", "");
_issuer = getString(cursor, "issuer", "");
_counter = getLong(cursor, "counter");
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_secret = SqlImporterHelper.getString(cursor, "secret");
_email = SqlImporterHelper.getString(cursor, "email", "");
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
_counter = SqlImporterHelper.getLong(cursor, "counter");
}
public int getType() {
@@ -0,0 +1,78 @@
package com.beemdevelopment.aegis.importers;
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;
public class GoogleAuthUriImporter extends DatabaseImporter {
public GoogleAuthUriImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (!line.isEmpty()) {
lines.add(line);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new GoogleAuthUriImporter.State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public DatabaseImporter.Result convert() {
DatabaseImporter.Result result = new DatabaseImporter.Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
return new VaultEntry(info);
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -0,0 +1,132 @@
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.Base64;
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.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
private static final String _subPath = "databases/PhoneFactor";
private static final String _pkgName = "com.azure.authenticator";
private static final int TYPE_TOTP = 0;
private static final int TYPE_MICROSOFT = 1;
public MicrosoftAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(getContext());
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);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
int type = sqlEntry.getType();
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret;
int digits = 6;
switch (entry.getType()) {
case TYPE_TOTP:
secret = Base32.decode(entry.getSecret());
break;
case TYPE_MICROSOFT:
digits = 8;
secret = Base64.decode(entry.getSecret());
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
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());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _issuer;
private String _userName;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "account_type");
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
_issuer = SqlImporterHelper.getString(cursor, "name");
_userName = SqlImporterHelper.getString(cursor, "username");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getIssuer() {
return _issuer;
}
public String getUserName() {
return _userName;
}
}
}
@@ -0,0 +1,150 @@
package com.beemdevelopment.aegis.importers;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.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;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
public class SqlImporterHelper {
private Context _context;
public SqlImporterHelper(Context context) {
_context = context;
}
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)) {
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<>();
if (cursor.moveToFirst()) {
do {
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
entries.add(entry);
} while (cursor.moveToNext());
}
return entries;
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
}
}
@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) {
return def;
}
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));
}
public static abstract class Entry {
public Entry(Cursor cursor) {
}
}
}
@@ -3,18 +3,19 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.Base64Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
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 {
@@ -26,27 +27,23 @@ 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 {
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
return new State(obj);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
@@ -82,7 +79,7 @@ public class SteamImporter extends DatabaseImporter {
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | Base64Exception | OtpInfoException e) {
} catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
@@ -0,0 +1,234 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
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.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
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;
import org.json.JSONObject;
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;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TotpAuthenticatorImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
private static final String _pkgName = "com.authenticator.authservice2";
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
public TotpAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
String data = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(PREF_KEY)) {
data = entry.Value;
}
}
if (data == null) {
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
}
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = IOUtils.readAll(stream);
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
} catch (IOException | XmlPullParserException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List<JSONObject> parse(String data) throws JSONException {
JSONArray array = new JSONArray(data);
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
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
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = cipher.doFinal(_data);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
JSONArray keys = obj.names();
List<JSONObject> entries = new ArrayList<>();
if (keys != null && keys.length() > 0) {
entries = parse((String) keys.get(0));
}
return new DecryptedState(entries);
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException
| JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setMessage(R.string.choose_totpauth_importer)
.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);
})
.create());
}
private void decrypt(char[] password, DecryptListener listener) {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}
}
public static class DecryptedState extends DatabaseImporter.State {
private List<JSONObject> _objs;
private DecryptedState(List<JSONObject> objs) {
super(false);
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _objs) {
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 {
int base = obj.getInt("base");
String secretString = obj.getString("key");
byte[] secret;
switch (base) {
case 16:
secret = Hex.decode(secretString);
break;
case 32:
secret = Base32.decode(secretString);
break;
case 64:
secret = Base64.decode(secretString);
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
return new VaultEntry(info, name, issuer);
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}
@@ -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());
}
}
}
}
@@ -1,14 +1,11 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
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) {
@@ -16,68 +13,35 @@ 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 {
ArrayList<String> lines = new ArrayList<>();
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(reader.getStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new State(lines);
public WinAuthImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(getContext());
DatabaseImporter.State state = importer.read(stream);
return new State(state);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private DatabaseImporter.State _state;
private State(ArrayList<String> lines) {
private State(DatabaseImporter.State state) {
super(false);
_lines = lines;
_state = state;
}
@Override
public Result convert() {
Result result = new Result();
public Result convert() throws DatabaseImporterException {
Result result = _state.convert();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
for (VaultEntry entry : result.getEntries()) {
entry.setIssuer(entry.getName());
entry.setName("WinAuth");
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
VaultEntry vaultEntry = new VaultEntry(info);
vaultEntry.setIssuer(vaultEntry.getName());
vaultEntry.setName("WinAuth");
return vaultEntry;
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}
@@ -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";
}
}
@@ -2,10 +2,20 @@ package com.beemdevelopment.aegis.otp;
import android.net.Uri;
import com.beemdevelopment.aegis.GoogleAuthProtos;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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 implements Serializable {
public static final String SCHEME = "otpauth";
public static final String SCHEME_EXPORT = "otpauth-migration";
public class GoogleAuthInfo {
private OtpInfo _info;
private String _accountName;
private String _issuer;
@@ -22,21 +32,25 @@ public class GoogleAuthInfo {
public Uri getUri() {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
builder.scheme(SCHEME);
if (_info instanceof TotpInfo) {
builder.authority("totp");
if (_info instanceof SteamInfo) {
builder.authority("steam");
} else {
builder.authority("totp");
}
builder.appendQueryParameter("period", Integer.toString(((TotpInfo)_info).getPeriod()));
} else if (_info instanceof HotpInfo) {
builder.authority("hotp");
builder.appendQueryParameter("counter", Long.toString(((HotpInfo)_info).getCounter()));
} else {
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
}
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));
@@ -51,35 +65,37 @@ public class GoogleAuthInfo {
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (uri == null) {
throw new GoogleAuthInfoException("bad uri format");
throw new GoogleAuthInfoException(uri, String.format("Bad URI format: %s", s));
}
return GoogleAuthInfo.parseUri(uri);
}
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("otpauth")) {
throw new GoogleAuthInfoException("unsupported protocol");
if (scheme == null || !scheme.equals(SCHEME)) {
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("'secret' is not set");
throw new GoogleAuthInfoException(uri, "Parameter 'secret' is not present");
}
// decode secret
byte[] secret;
try {
secret = Base32.decode(encodedSecret.toCharArray());
} catch (Base32Exception e) {
throw new GoogleAuthInfoException("bad secret", e);
secret = parseSecret(encodedSecret);
} catch (EncodingException 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(uri, String.format("Host not present in URI: %s", uri.toString()));
}
switch (type) {
case "totp":
TotpInfo totpInfo = new TotpInfo(secret);
@@ -89,20 +105,28 @@ public class GoogleAuthInfo {
}
info = totpInfo;
break;
case "steam":
SteamInfo steamInfo = new SteamInfo(secret);
period = uri.getQueryParameter("period");
if (period != null) {
steamInfo.setPeriod(Integer.parseInt(period));
}
info = steamInfo;
break;
case "hotp":
HotpInfo hotpInfo = new HotpInfo(secret);
String counter = uri.getQueryParameter("counter");
if (counter == null) {
throw new GoogleAuthInfoException("'counter' was not set");
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 e) {
throw new GoogleAuthInfoException(e);
} catch (OtpInfoException | NumberFormatException e) {
throw new GoogleAuthInfoException(uri, e);
}
// provider info used to disambiguate accounts
@@ -141,13 +165,120 @@ public class GoogleAuthInfo {
if (digits != null) {
info.setDigits(Integer.parseInt(digits));
}
} catch (OtpInfoException e) {
throw new GoogleAuthInfoException(e);
} catch (OtpInfoException | NumberFormatException 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(uri, "Bad URI format");
}
return GoogleAuthInfo.parseExportUri(uri);
}
public static Export parseExportUri(Uri uri) throws GoogleAuthInfoException {
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals(SCHEME_EXPORT)) {
throw new GoogleAuthInfoException(uri, "Unsupported protocol");
}
String host = uri.getHost();
if (host == null || !host.equals("offline")) {
throw new GoogleAuthInfoException(uri, "Unsupported host");
}
String data = uri.getQueryParameter("data");
if (data == null) {
throw new GoogleAuthInfoException(uri, "Parameter 'data' is not set");
}
GoogleAuthProtos.MigrationPayload payload;
try {
byte[] bytes = Base64.decode(data);
payload = GoogleAuthProtos.MigrationPayload.parseFrom(bytes);
} catch (EncodingException | InvalidProtocolBufferException e) {
throw new GoogleAuthInfoException(uri, e);
}
List<GoogleAuthInfo> infos = new ArrayList<>();
for (GoogleAuthProtos.MigrationPayload.OtpParameters params : payload.getOtpParametersList()) {
OtpInfo otp;
try {
int digits;
switch (params.getDigits()) {
case DIGIT_COUNT_UNSPECIFIED:
// intentional fallthrough
case DIGIT_COUNT_SIX:
digits = TotpInfo.DEFAULT_DIGITS;
break;
case DIGIT_COUNT_EIGHT:
digits = 8;
break;
default:
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(uri, e);
}
String name = params.getName();
String issuer = params.getIssuer();
int colonI = name.indexOf(':');
if (issuer.isEmpty() && colonI != -1) {
issuer = name.substring(0, colonI);
name = name.substring(colonI + 1);
}
GoogleAuthInfo info = new GoogleAuthInfo(otp, name, issuer);
infos.add(info);
}
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
}
public String getIssuer() {
return _issuer;
}
@@ -155,4 +286,34 @@ public class GoogleAuthInfo {
public String getAccountName() {
return _accountName;
}
public static class Export {
private int _batchId;
private int _batchIndex;
private int _batchSize;
private List<GoogleAuthInfo> _entries;
public Export(List<GoogleAuthInfo> entries, int batchId, int batchIndex, int batchSize) {
_batchId = batchId;
_batchIndex = batchIndex;
_batchSize = batchSize;
_entries = entries;
}
public List<GoogleAuthInfo> getEntries() {
return _entries;
}
public int getBatchSize() {
return _batchSize;
}
public int getBatchIndex() {
return _batchIndex;
}
public int getBatchId() {
return _batchId;
}
}
}
@@ -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 {
@@ -34,12 +35,12 @@ public class HotpInfo extends OtpInfo {
OTP otp = HOTP.generateOTP(getSecret(), getAlgorithm(true), getDigits(), getCounter());
return otp.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException();
throw new RuntimeException(e);
}
}
@Override
public String getType() {
public String getTypeId() {
return ID;
}
@@ -1,7 +1,7 @@
package com.beemdevelopment.aegis.otp;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
import com.beemdevelopment.aegis.encoding.EncodingException;
import org.json.JSONException;
import org.json.JSONObject;
@@ -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();
@@ -93,7 +100,7 @@ public abstract class OtpInfo implements Serializable {
OtpInfo info;
try {
byte[] secret = Base32.decode(obj.getString("secret").toCharArray());
byte[] secret = Base32.decode(obj.getString("secret"));
String algo = obj.getString("algo");
int digits = obj.getInt("digits");
@@ -110,7 +117,7 @@ public abstract class OtpInfo implements Serializable {
default:
throw new OtpInfoException("unsupported otp type: " + type);
}
} catch (Base32Exception | JSONException e) {
} catch (EncodingException | JSONException e) {
throw new OtpInfoException(e);
}
@@ -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,10 +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.StringRes;
import androidx.core.view.LayoutInflaterCompat;
@@ -17,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;
@@ -39,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());
@@ -71,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);
});
}
@@ -107,22 +127,42 @@ 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;
String backgroundColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(backgroundColorResource, getTheme())));
String textColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.primaryText, getTheme())));
String licenseColor = String.format("%06X", (0xFFFFFF & ThemeHelper.getThemeColor(R.attr.cardBackgroundFocused, getTheme())));
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);
String linkColor = getThemeColorAsHex(R.attr.colorAccent);
stylesheet = String.format(stylesheet, backgroundColor, textColor, licenseColor);
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()
.show();
}
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,27 +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 androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
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) {
@@ -29,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;
}
@@ -46,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();
}
}
@@ -88,20 +92,35 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
return _app.getPreferences();
}
protected void setPreferredTheme(Theme theme) {
_currentTheme = theme;
/**
* Called when the activity is expected to set its theme.
*/
protected void onSetTheme() {
setTheme(ThemeMap.DEFAULT);
}
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;
/**
* 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) {
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
}
protected void setLocale(Locale locale) {
@@ -110,36 +129,87 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
Configuration config = new Configuration();
config.locale = locale;
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
this.getResources().updateConfiguration(config, this.getResources().getDisplayMetrics());
}
/**
* Reports whether this Activity has been resumed. (i.e. onResume was called)
*/
protected boolean isOpen() {
return _resumed;
protected boolean saveVault(boolean backup) {
try {
getApp().getVaultManager().save(backup);
return true;
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
return false;
}
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was locked by an external trigger while the Activity was still open.
*/
private boolean isOrphan() {
return !(this instanceof MainActivity) && _app.getVaultManager().isLocked();
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));
}
}
}
@@ -2,55 +2,79 @@ package com.beemdevelopment.aegis.ui;
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;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.AegisApplication;
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.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.SlotIntegrityException;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.beemdevelopment.aegis.helpers.BiometricsHelper;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.UiThreadExecutor;
import com.beemdevelopment.aegis.ui.tasks.SlotListTask;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity implements SlotListTask.Callback {
public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private BiometricPrompt.CryptoObject _bioCryptoObj;
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;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
_prefs = new Preferences(this);
setContentView(R.layout.activity_auth);
_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)) {
@@ -59,9 +83,25 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
return false;
});
if (_prefs.isPinKeyboardEnabled()) {
_textPassword.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
}
Intent intent = getIntent();
_slots = (SlotList) intent.getSerializableExtra("slots");
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
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
if (_slots.has(BiometricSlot.class) && BiometricsHelper.isAvailable(this)) {
@@ -80,57 +120,50 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
continue;
}
Cipher cipher = slot.createDecryptCipher(key);
_bioCryptoObj = new BiometricPrompt.CryptoObject(cipher);
_bioPrompt = new BiometricPrompt(this, new UiThreadExecutor(), new BiometricPromptListener());
_bioSlot = slot;
_bioKey = key;
biometricsButton.setVisibility(View.VISIBLE);
invalidated = false;
break;
}
}
} catch (KeyStoreHandleException | SlotException e) {
throw new RuntimeException(e);
} catch (KeyStoreHandleException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.biometric_init_error, e);
}
// display a help message if a matching invalidated keystore entry was found
if (invalidated) {
boxBiometricInfo.setVisibility(View.VISIBLE);
biometricsButton.setVisibility(View.GONE);
}
}
decryptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
decryptButton.setOnClickListener(v -> {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
char[] password = EditTextHelper.getEditTextChars(_textPassword);
trySlots(PasswordSlot.class, password);
}
char[] password = EditTextHelper.getEditTextChars(_textPassword);
List<PasswordSlot> slots = _slots.findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(AuthActivity.this, new PasswordDerivationListener());
task.execute(getLifecycle(), params);
});
biometricsButton.setOnClickListener(v -> {
showBiometricPrompt();
});
if (_bioCryptoObj == null) {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
}
private void showError() {
Dialogs.showSecureDialog(new AlertDialog.Builder(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());
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("inhibitBioPrompt", _inhibitBioPrompt);
}
private <T extends Slot> void trySlots(Class<T> type, Object obj) {
SlotListTask.Params params = new SlotListTask.Params(_slots, obj);
new SlotListTask<>(type, this, this).execute(params);
@Override
protected void onSetTheme() {
setTheme(ThemeMap.NO_ACTION_BAR);
}
private void selectPassword() {
@@ -142,59 +175,144 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
@Override
public void onBackPressed() {
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
Intent intent = new Intent();
setResult(RESULT_CANCELED, intent);
finish();
}
finishAffinity();
}
@Override
public void onResume() {
super.onResume();
if (_bioPrompt != null) {
showBiometricPrompt();
boolean remindPassword = _prefs.isPasswordReminderNeeded();
if (_bioKey == null || remindPassword) {
focusPasswordField();
}
}
public void showBiometricPrompt() {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.authentication))
.setNegativeButtonText(getString(android.R.string.cancel))
.build();
_bioPrompt.authenticate(info, _bioCryptoObj);
if (_bioKey != null && _bioPrompt == null && !_inhibitBioPrompt && !remindPassword) {
_bioPrompt = showBiometricPrompt();
}
_inhibitBioPrompt = false;
}
@Override
public void onPause() {
super.onPause();
if (_bioPrompt != null) {
if (!isChangingConfigurations() && _bioPrompt != null) {
_bioPrompt.cancelAuthentication();
_bioPrompt = null;
}
super.onPause();
}
@Override
public void onTaskFinished(SlotListTask.Result result) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
}
public void onAttachedToWindow() {
if (_bioKey != null && _prefs.isPasswordReminderNeeded()) {
showPasswordReminder();
}
}
// send the master key back to the main activity
Intent intent = new Intent();
intent.putExtra("creds", new VaultFileCredentials(result.getKey(), _slots));
intent.putExtra("repairedSlot", result.isSlotRepaired());
setResult(RESULT_OK, intent);
finish();
} else {
showError();
private void focusPasswordField() {
_textPassword.requestFocus();
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
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(() -> {
// 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 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 null;
}
BiometricPrompt.CryptoObject cryptoObj = new BiometricPrompt.CryptoObject(cipher);
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();
prompt.authenticate(info, cryptoObj);
return prompt;
}
private void finish(MasterKey key, boolean isSlotRepaired) {
VaultFileCredentials creds = new VaultFileCredentials(key, _slots);
try {
AegisApplication app = getApp();
app.initVaultManager(app.loadVaultFile(), creds);
if (isSlotRepaired) {
saveVault(true);
}
} 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) {
if (result != null) {
// replace the old slot with the repaired one
if (result.isSlotRepaired()) {
_slots.replace(result.getSlot());
}
if (result.getSlot().getType() == Slot.TYPE_DERIVED) {
_prefs.resetPasswordReminderTimestamp();
}
finish(result.getKey(), result.isSlotRepaired());
} else {
onInvalidPassword();
}
}
}
@@ -202,6 +320,8 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
@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();
}
@@ -210,7 +330,20 @@ public class AuthActivity extends AegisActivity implements SlotListTask.Callback
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
trySlots(BiometricSlot.class, _bioCryptoObj.getCipher());
_bioPrompt = null;
MasterKey key;
BiometricSlot slot = _slots.find(BiometricSlot.class);
try {
key = slot.getKey(result.getCryptoObject().getCipher());
} catch (SlotException | SlotIntegrityException e) {
e.printStackTrace();
Dialogs.showErrorDialog(AuthActivity.this, R.string.biometric_decrypt_error, e);
return;
}
finish(key, false);
}
@Override
@@ -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,217 +0,0 @@
package com.beemdevelopment.aegis.ui;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.NumberPicker;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
public class Dialogs {
private Dialogs() {
}
public static void secureDialog(Dialog dialog) {
dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
public static void showSecureDialog(Dialog dialog) {
if (new Preferences(dialog.getContext()).isSecureScreenEnabled()) {
secureDialog(dialog);
}
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, DialogInterface.OnClickListener onDelete, int totalEntries) {
String title, message;
if (totalEntries > 1) {
title = activity.getString(R.string.delete_entries);
message = String.format(activity.getString(R.string.delete_entries_description), totalEntries);
} else {
title = activity.getString(R.string.delete_entry);
message = activity.getString(R.string.delete_entry_description);
}
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.yes, onDelete)
.setNegativeButton(android.R.string.no, null)
.create());
}
public static void showDiscardDialog(Activity activity, DialogInterface.OnClickListener onSave, DialogInterface.OnClickListener onDiscard) {
showSecureDialog(new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.discard_changes))
.setMessage(activity.getString(R.string.discard_changes_description))
.setPositiveButton(R.string.save, onSave)
.setNegativeButton(R.string.discard, onDiscard)
.create());
}
public static void showSetPasswordDialog(Activity activity, Dialogs.SlotListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_password, null);
EditText textPassword = view.findViewById(R.id.text_password);
EditText textPasswordConfirm = view.findViewById(R.id.text_password_confirm);
CheckBox switchToggleVisibility = view.findViewById(R.id.check_toggle_visibility);
switchToggleVisibility.setOnCheckedChangeListener((CompoundButton.OnCheckedChangeListener) (buttonView, isChecked) -> {
if (isChecked) {
textPassword.setTransformationMethod(null);
textPassword.clearFocus();
textPasswordConfirm.setEnabled(false);
} else {
textPassword.setTransformationMethod(new PasswordTransformationMethod());
textPasswordConfirm.setEnabled(true);
}
});
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_password)
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel, null)
.create();
final AtomicReference<Button> buttonOK = new AtomicReference<>();
dialog.setOnShowListener(d -> {
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setEnabled(false);
buttonOK.set(button);
// replace the default listener
button.setOnClickListener(v -> {
if (!EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm) && !switchToggleVisibility.isChecked()) {
return;
}
char[] password = EditTextHelper.getEditTextChars(textPassword);
PasswordSlot slot = new PasswordSlot();
DerivationTask task = new DerivationTask(activity, key -> {
Cipher cipher;
try {
cipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
listener.onException(e);
dialog.cancel();
return;
}
listener.onSlotResult(slot, cipher);
dialog.dismiss();
});
task.execute(new DerivationTask.Params(slot, password));
});
});
TextWatcher watcher = new TextWatcher() {
public void onTextChanged(CharSequence c, int start, int before, int count) {
boolean equal = EditTextHelper.areEditTextsEqual(textPassword, textPasswordConfirm);
buttonOK.get().setEnabled(equal || switchToggleVisibility.isChecked());
}
public void beforeTextChanged(CharSequence c, int start, int count, int after) {
}
public void afterTextChanged(Editable c) {
}
};
textPassword.addTextChangedListener(watcher);
textPasswordConfirm.addTextChangedListener(watcher);
showSecureDialog(dialog);
}
private static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener, boolean isSecret) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_text_input, null);
EditText input = view.findViewById(R.id.text_input);
if (isSecret) {
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
input.setHint(hintId);
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(titleId)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
char[] text = EditTextHelper.getEditTextChars(input);
listener.onTextInputResult(text);
})
.create();
showSecureDialog(dialog);
}
public static void showTextInputDialog(Context context, @StringRes int titleId, @StringRes int hintId, TextInputListener listener) {
showTextInputDialog(context, titleId, hintId, listener, false);
}
public static void showPasswordInputDialog(Context context, TextInputListener listener) {
showTextInputDialog(context, R.string.set_password, R.string.password, listener, true);
}
public static void showNumberPickerDialog(Activity activity, NumberInputListener listener) {
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
numberPicker.setMinValue(3);
numberPicker.setMaxValue(60);
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getTapToRevealTime());
numberPicker.setWrapSelectorWheel(true);
AlertDialog dialog = new AlertDialog.Builder(activity)
.setTitle(R.string.set_number)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
listener.onNumberInputResult(numberPicker.getValue()))
.create();
showSecureDialog(dialog);
}
public interface NumberInputListener {
void onNumberInputResult(int number);
}
public interface TextInputListener {
void onTextInputResult(char[] text);
}
public interface SlotListener {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
}
@@ -1,97 +1,126 @@
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.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.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.vault.VaultEntry;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base32Exception;
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.beemdevelopment.aegis.util.Cloner;
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.text.Collator;
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 androidx.annotation.ArrayRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import de.hdodenhof.circleimageview.CircleImageView;
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;
private RelativeLayout _advancedSettingsHeader;
private RelativeLayout _advancedSettings;
private VaultManager _vault;
@Override
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();
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
@@ -99,44 +128,64 @@ public class EditEntryActivity extends AegisActivity {
// retrieve info from the calling activity
Intent intent = getIntent();
_origEntry = (VaultEntry) intent.getSerializableExtra("entry");
_isNew = intent.getBooleanExtra("isNew", false);
_groups = new TreeSet<>(Collator.getInstance());
_groups.addAll(intent.getStringArrayListExtra("groups"));
if (_isNew) {
setTitle(R.string.add_new_profile);
UUID entryUUID = (UUID) intent.getSerializableExtra("entryUUID");
if (entryUUID != null) {
_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);
@@ -151,27 +200,25 @@ 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();
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", info.getClass()));
}
_textDigits.setText(Integer.toString(info.getDigits()));
byte[] secretBytes = _origEntry.getInfo().getSecret();
if (secretBytes != null) {
char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
String secretString = Base32.encode(secretBytes);
_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);
@@ -181,88 +228,74 @@ 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();
switch (type.toLowerCase()) {
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();
}
_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() {
Animation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new AccelerateInterpolator());
@@ -309,16 +342,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<>();
@@ -343,7 +381,7 @@ public class EditEntryActivity extends AegisActivity {
return;
}
finish(entry.get(), false);
addAndFinish(entry.get());
},
(dialog, which) -> super.onBackPressed()
);
@@ -359,13 +397,18 @@ public class EditEntryActivity extends AegisActivity {
onSave();
break;
case R.id.action_delete:
Dialogs.showDeleteEntryDialog(this, (dialog, which) -> {
finish(_origEntry, true);
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:
@@ -375,6 +418,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);
@@ -388,46 +520,54 @@ public class EditEntryActivity extends AegisActivity {
return true;
}
private void finish(VaultEntry entry, boolean delete) {
private void addAndFinish(VaultEntry entry) {
// 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);
}
saveAndFinish(entry, false);
}
private void deleteAndFinish(VaultEntry entry) {
_vault.removeEntry(entry);
saveAndFinish(entry, true);
}
private void saveAndFinish(VaultEntry entry, boolean delete) {
Intent intent = new Intent();
intent.putExtra("entry", entry);
intent.putExtra("entryUUID", entry.getUUID());
intent.putExtra("delete", delete);
setResult(RESULT_OK, intent);
finish();
if (saveVault(true)) {
setResult(RESULT_OK, intent);
finish();
}
}
@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) {
DocumentFile file = DocumentFile.fromSingleUri(this, data.getData());
if (file.getType().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);
@@ -435,7 +575,7 @@ public class EditEntryActivity extends AegisActivity {
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.");
}
@@ -446,8 +586,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 {
@@ -458,8 +598,12 @@ public class EditEntryActivity extends AegisActivity {
byte[] secret;
try {
secret = Base32.decode(EditTextHelper.getEditTextChars(_textSecret, true));
} catch (Base32Exception e) {
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
secret = GoogleAuthInfo.parseSecret(secretString);
if (secret.length == 0) {
throw new ParseException("Secret cannot be empty");
}
} catch (EncodingException e) {
throw new ParseException("Secret is not valid base32.");
}
@@ -475,14 +619,14 @@ 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.");
}
info = new HotpInfo(secret, algo, digits, counter);
break;
default:
throw new RuntimeException();
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
}
info.setDigits(digits);
@@ -496,9 +640,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);
@@ -506,13 +650,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);
}
}
@@ -528,6 +684,10 @@ public class EditEntryActivity extends AegisActivity {
}
private boolean onSave() {
if (_isEditingIcon) {
stopEditingIcon(true);
}
VaultEntry entry;
try {
entry = parseEntry();
@@ -536,11 +696,22 @@ public class EditEntryActivity extends AegisActivity {
return false;
}
finish(entry, false);
addAndFinish(entry);
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) {
}
@@ -558,19 +729,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,13 +1,17 @@
package com.beemdevelopment.aegis.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
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;
@@ -16,181 +20,86 @@ import com.beemdevelopment.aegis.vault.VaultManager;
import com.beemdevelopment.aegis.vault.VaultManagerException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticatedSlide;
import com.beemdevelopment.aegis.ui.slides.CustomAuthenticationSlide;
import com.beemdevelopment.aegis.ui.tasks.DerivationTask;
import com.github.paolorotolo.appintro.AppIntro2;
import com.github.paolorotolo.appintro.AppIntroFragment;
import com.github.paolorotolo.appintro.model.SliderPage;
import org.json.JSONObject;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
public class IntroActivity extends AppIntro2 implements DerivationTask.Callback {
public static final int RESULT_OK = 0;
public static final int RESULT_EXCEPTION = 1;
private CustomAuthenticatedSlide _authenticatedSlide;
private CustomAuthenticationSlide _authenticationSlide;
private Fragment _endSlide;
private Vault _vault;
private VaultFile _vaultFile;
private PasswordSlot _passwordSlot;
private Cipher _passwordCipher;
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);
// set FLAG_SECURE on the window of every IntroActivity
_prefs = new Preferences(this);
if (_prefs.isSecureScreenEnabled()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
setWizardMode(true);
showSkipButton(false);
pager.setPagingEnabled(false);
//showPagerIndicator(false);
setGoBackLock(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.setDescColor(getResources().getColor(R.color.primary_text_dark));
homeSliderPage.setBgColor(getResources().getColor(R.color.colorSecondary));
addSlide(AppIntroFragment.newInstance(homeSliderPage));
_authenticationSlide = new CustomAuthenticationSlide();
_authenticationSlide.setBgColor(getResources().getColor(R.color.colorSecondary));
//_authenticationSlide.setDescColor(getResources().getColor(R.color.primary_text_dark));
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.setBgColor(getResources().getColor(R.color.colorSecondary));
_endSlide = AppIntroFragment.newInstance(endSliderPage);
addSlide(_endSlide);
_vault = new Vault();
_vaultFile = new VaultFile();
}
private void setException(Exception e) {
Intent result = new Intent();
result.putExtra("exception", e);
setResult(RESULT_EXCEPTION, result);
finish();
addSlide(WelcomeSlide.class);
addSlide(SecurityPickerSlide.class);
addSlide(SecuritySetupSlide.class);
addSlide(DoneSlide.class);
}
@Override
public void onSlideChanged(Fragment oldFragment, Fragment newFragment) {
Intent intent = getIntent();
int cryptType = intent.getIntExtra("cryptType", CustomAuthenticationSlide.CRYPT_TYPE_INVALID);
if (newFragment == _endSlide && cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_passwordSlot = new PasswordSlot();
DerivationTask.Params params = new DerivationTask.Params(_passwordSlot, _authenticatedSlide.getPassword());
new DerivationTask(this, this).execute(params);
} else if (oldFragment == _authenticationSlide && newFragment != _endSlide) {
// skip to the last slide if no encryption will be used
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// TODO: no magic indices
getPager().setCurrentItem(4);
}
}
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();
// wait for the key derivation background task
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE &&
(_passwordSlot == null || _passwordCipher == null)) {
return;
if (oldSlide == SecurityPickerSlide.class
&& newSlide == SecuritySetupSlide.class
&& getState().getInt("cryptType", CRYPT_TYPE_INVALID) == CRYPT_TYPE_NONE) {
skipToSlide(DoneSlide.class);
return true;
}
// generate the master key
VaultFileCredentials creds = null;
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
creds = new VaultFileCredentials();
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));
}
if (cryptType != CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
// encrypt the master key with a key derived from the user's password
// and add it to the list of slots
if (_passwordSlot == null || _passwordCipher == null) {
throw new RuntimeException();
}
try {
_passwordSlot.setKey(creds.getKey(), _passwordCipher);
creds.getSlots().add(_passwordSlot);
} catch (SlotException e) {
setException(e);
}
}
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_BIOMETRIC) {
BiometricSlot slot = _authenticatedSlide.getBiometricSlot();
try {
slot.setKey(creds.getKey(), _authenticatedSlide.getBiometriCipher());
} catch (SlotException e) {
setException(e);
}
creds.getSlots().add(slot);
}
// finally, save the vault
Vault vault = new Vault();
VaultFile vaultFile = new VaultFile();
try {
JSONObject obj = _vault.toJson();
if (cryptType == CustomAuthenticationSlide.CRYPT_TYPE_NONE) {
_vaultFile.setContent(obj);
JSONObject obj = vault.toJson();
if (cryptType == CRYPT_TYPE_NONE) {
vaultFile.setContent(obj);
} else {
_vaultFile.setContent(obj, creds);
vaultFile.setContent(obj, creds);
}
VaultManager.save(getApplicationContext(), _vaultFile);
VaultManager.save(getApplicationContext(), vaultFile);
} catch (VaultManagerException | VaultFileException e) {
setException(e);
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_init_error, e);
return;
}
// send the master key back to the main activity
Intent result = new Intent();
result.putExtra("creds", creds);
setResult(RESULT_OK, result);
if (cryptType == CRYPT_TYPE_NONE) {
getApp().initVaultManager(vault, null);
} else {
getApp().initVaultManager(vault, creds);
}
// skip the intro from now on
_prefs.setIntroDone(true);
getPreferences().setIntroDone(true);
setResult(RESULT_OK);
finish();
}
@Override
public void onTaskFinished(SecretKey key) {
if (key != null) {
try {
_passwordCipher = Slot.createEncryptCipher(key);
} catch (SlotException e) {
setException(e);
}
} else {
setException(new NullPointerException());
}
}
}
@@ -7,61 +7,68 @@ 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.VaultFileCredentials;
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
private static final int CODE_SCAN = 0;
private static final int CODE_ADD_ENTRY = 1;
private static final int CODE_EDIT_ENTRY = 2;
private static final int CODE_ENTER_ENTRY = 3;
private static final int CODE_DO_INTRO = 4;
private static final int CODE_DECRYPT = 5;
private static final int CODE_PREFERENCES = 6;
private static final int CODE_SCAN_IMAGE = 7;
private static final int CODE_DO_INTRO = 3;
private static final int CODE_DECRYPT = 4;
private static final int CODE_PREFERENCES = 5;
private static final int CODE_SCAN_IMAGE = 6;
// permission request codes
private static final int CODE_PERM_CAMERA = 0;
@@ -70,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;
@@ -88,110 +97,103 @@ 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());
// set up the floating action button
_fabMenu = findViewById(R.id.fab);
findViewById(R.id.fab_enter).setOnClickListener(view -> {
_fabMenu.collapse();
startEditProfileActivity(CODE_ENTER_ENTRY, null, true);
});
findViewById(R.id.fab_scan_image).setOnClickListener(view -> {
_fabMenu.collapse();
startScanImageActivity();
});
findViewById(R.id.fab_scan).setOnClickListener(view -> {
_fabMenu.collapse();
startScanActivity();
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) {
if (data == null) {
return;
}
_isAuthenticating = false;
_isDoingIntro = false;
// don't process any activity results if the vault is locked
if (requestCode != CODE_DECRYPT && requestCode != CODE_DO_INTRO && _vault.isLocked()) {
if (resultCode != RESULT_OK) {
return;
}
switch (requestCode) {
case CODE_SCAN:
onScanResult(resultCode, data);
onScanResult(data);
break;
case CODE_ADD_ENTRY:
onAddEntryResult(resultCode, data);
onAddEntryResult(data);
break;
case CODE_EDIT_ENTRY:
onEditEntryResult(resultCode, data);
break;
case CODE_ENTER_ENTRY:
onEnterEntryResult(resultCode, data);
onEditEntryResult(data);
break;
case CODE_DO_INTRO:
onDoIntroResult(resultCode, data);
onDoIntroResult();
break;
case CODE_DECRYPT:
onDecryptResult(resultCode, data);
onDecryptResult();
break;
case CODE_PREFERENCES:
onPreferencesResult(resultCode, data);
onPreferencesResult(data);
break;
case CODE_SCAN_IMAGE:
onScanImageResult(resultCode, data);
onScanImageResult(data);
}
super.onActivityResult(requestCode, resultCode, data);
@@ -214,135 +216,120 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
}
private void onPreferencesResult(int resultCode, Intent data) {
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();
boolean searchAccountName = getPreferences().isSearchAccountNameEnabled();
boolean highlightEntry = getPreferences().isEntryHighlightEnabled();
boolean tapToReveal = getPreferences().isTapToRevealEnabled();
int tapToRevealTime = getPreferences().getTapToRevealTime();
ViewMode viewMode = getPreferences().getCurrentViewMode();
_entryListView.setShowAccountName(showAccountName);
_entryListView.setSearchAccountName(searchAccountName);
_entryListView.setHighlightEntry(highlightEntry);
_entryListView.setTapToReveal(tapToReveal);
_entryListView.setTapToRevealTime(tapToRevealTime);
_entryListView.setViewMode(viewMode);
_entryListView.refresh(true);
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 startEditProfileActivity(int requestCode, VaultEntry entry, boolean isNew) {
private void startEditEntryActivityForNew(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entry", entry != null ? entry : VaultEntry.getDefault());
intent.putExtra("isNew", isNew);
intent.putExtra("selectedGroup", _selectedGroup);
intent.putExtra("groups", new ArrayList<>(_vault.getGroups()));
intent.putExtra("newEntry", entry);
intent.putExtra("isManual", false);
startActivityForResult(intent, requestCode);
}
private void onScanResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
}
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 onAddEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveVault();
}
private void startEditEntryActivity(int requestCode, VaultEntry entry) {
Intent intent = new Intent(this, EditEntryActivity.class);
intent.putExtra("entryUUID", entry.getUUID());
startActivityForResult(intent, requestCode);
}
private void onEditEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
if (data.getBooleanExtra("delete", false)) {
deleteEntry(entry);
} else {
// this profile has been serialized/deserialized and is no longer the same instance it once was
// to deal with this, the replaceEntry functions are used
VaultEntry oldEntry = _vault.replaceEntry(entry);
_entryListView.replaceEntry(oldEntry, entry);
saveVault();
}
}
}
private void onEnterEntryResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
VaultEntry entry = (VaultEntry) data.getSerializableExtra("entry");
addEntry(entry);
saveVault();
}
}
private void onScanImageResult(int resultCode, Intent intent) {
if (resultCode == RESULT_OK) {
Uri inputFile = (intent.getData());
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
private void onScanResult(Intent data) {
List<VaultEntry> entries = (ArrayList<VaultEntry>) data.getSerializableExtra("entries");
if (entries.size() == 1) {
startEditEntryActivityForNew(CODE_ADD_ENTRY, entries.get(0));
} else {
for (VaultEntry entry : entries) {
_vault.addEntry(entry);
if (_loaded) {
_entryListView.addEntry(entry);
}
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
saveVault(true);
}
}
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
private void onAddEntryResult(Intent data) {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.addEntry(entry, true);
}
}
Reader reader = new MultiFormatReader();
Result result = reader.decode(binaryBitmap);
private void onEditEntryResult(Intent data) {
if (_loaded) {
UUID entryUUID = (UUID) data.getSerializableExtra("entryUUID");
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(CODE_ADD_ENTRY, entry, true);
} catch (NotFoundException | IOException | ChecksumException | FormatException | GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
if (data.getBooleanExtra("delete", false)) {
_entryListView.removeEntry(entryUUID);
} else {
VaultEntry entry = _vault.getEntryByUUID(entryUUID);
_entryListView.replaceEntry(entryUUID, entry);
}
}
}
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;
private void onScanImageResult(Intent intent) {
decodeQrCodeImage(intent.getData());
}
private void decodeQrCodeImage(Uri inputFile) {
Bitmap bitmap;
try {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
try (InputStream inputStream = getContentResolver().openInputStream(inputFile)) {
bitmap = BitmapFactory.decodeStream(inputStream, null, bmOptions);
bitmap = BitmapHelper.resize(bitmap, QrCodeAnalyzer.RESOLUTION.getWidth(), QrCodeAnalyzer.RESOLUTION.getHeight());
}
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);
}
int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
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);
}
}
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray);
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(source));
if (groups.size() > 0) {
menu.add(R.id.action_filter_group, Menu.NONE, 10, R.string.filter_ungrouped);
}
Reader reader = new QRCodeReader();
Result result = reader.decode(binaryBitmap);
menu.setGroupCheckable(R.id.action_filter_group, true, true);
GoogleAuthInfo info = GoogleAuthInfo.parseUri(result.getText());
VaultEntry entry = new VaultEntry(info);
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 updateSortCategoryMenu() {
@@ -350,39 +337,26 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_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();
checkTimeSyncSetting();
}
private void addEntry(VaultEntry entry) {
_vault.addEntry(entry);
_entryListView.addEntry(entry);
}
private void onDoIntroResult(int resultCode, Intent data) {
if (resultCode == IntroActivity.RESULT_EXCEPTION) {
// TODO: user feedback
Exception e = (Exception) data.getSerializableExtra("exception");
throw new RuntimeException(e);
private void checkTimeSyncSetting() {
boolean autoTime = Settings.Global.getInt(getContentResolver(), Settings.Global.AUTO_TIME, 1) == 1;
if (!autoTime && getPreferences().isTimeSyncWarningEnabled()) {
Dialogs.showTimeSyncWarningDialog(this, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_DATE_SETTINGS);
startActivity(intent);
});
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
unlockVault(creds);
}
private void onDecryptResult(int resultCode, Intent intent) {
VaultFileCredentials creds = (VaultFileCredentials) intent.getSerializableExtra("creds");
boolean unlocked = unlockVault(creds);
// save the vault in case a slot was repaired
if (unlocked && intent.getBooleanExtra("repairedSlot", false)) {
_vault.setCredentials(creds);
saveVault();
}
doShortcutActions();
private void onDecryptResult() {
_vault = _app.getVaultManager();
loadEntries();
checkTimeSyncSetting();
}
private void startScanActivity() {
@@ -398,14 +372,29 @@ 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() {
Intent intent = getIntent();
String action = intent.getStringExtra("action");
if (action == null || _vault.isLocked()) {
if (action == null || _app.isVaultLocked()) {
return;
}
@@ -419,64 +408,96 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
private void handleDeeplink() {
if (_vault.isLocked()) {
if (_app.isVaultLocked()) {
return;
}
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 {
info = GoogleAuthInfo.parseUri(uri);
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.unable_to_read_qrcode), Toast.LENGTH_SHORT).show();
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.unable_to_read_qrcode, e);
}
if (info != null) {
VaultEntry entry = new VaultEntry(info);
startEditProfileActivity(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.isLocked()) {
if (_vault == null) {
// start the intro if the vault file doesn't exist
if (!_vault.isLoaded() && !_vault.fileExists()) {
// the vault doesn't exist, start the intro
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;
} else {
unlockVault(null);
}
// read the vault from disk
// if this fails, show the error to the user and close the app
try {
VaultFile vaultFile = _app.loadVaultFile();
if (!vaultFile.isEncrypted()) {
_vault = _app.initVaultManager(vaultFile, null);
}
} catch (VaultManagerException e) {
e.printStackTrace();
Dialogs.showErrorDialog(this, R.string.vault_load_error, e, (dialog1, which) -> finish());
return;
}
}
if (_app.isVaultLocked()) {
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);
} else {
loadEntries();
checkTimeSyncSetting();
}
handleDeeplink();
handleSharedImage();
updateLockIcon();
doShortcutActions();
updateBackupErrorBar();
}
@Override
@@ -486,13 +507,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;
}
@@ -505,14 +525,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.removeEntry(oldEntry);
}
saveVault();
}
private void deleteEntry(VaultEntry entry) {
VaultEntry oldEntry = _vault.removeEntry(entry);
saveVault();
_entryListView.removeEntry(oldEntry);
saveVault(true);
}
@Override
@@ -521,7 +534,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();
}
@@ -562,8 +575,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: {
@@ -572,19 +584,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);
@@ -620,61 +622,40 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_searchView.setIconified(true);
}
private boolean unlockVault(VaultFileCredentials creds) {
try {
if (!_vault.isLoaded()) {
_vault.load();
}
if (_vault.isLocked()) {
if (creds == null) {
startAuthActivity();
return false;
} else {
_vault.unlock(creds);
}
}
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.decryption_error), Toast.LENGTH_LONG).show();
startAuthActivity();
return false;
}
loadEntries();
return true;
}
private void loadEntries() {
// load all entries
List<VaultEntry> entries = new ArrayList<VaultEntry>(_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("slots", _vault.getFileHeader().getSlots());
intent.putExtra("requiresUnlock", false);
intent.putExtra("cancelAction", CancelAction.KILL);
startActivityForResult(intent, CODE_DECRYPT);
}
private void saveVault() {
try {
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(this, getString(R.string.saving_error), Toast.LENGTH_LONG).show();
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() {
// hide the lock icon if the vault is not unlocked
if (_menu != null && !_vault.isLocked()) {
if (_menu != null && !_app.isVaultLocked()) {
MenuItem item = _menu.findItem(R.id.action_lock);
item.setVisible(_vault.isEncryptionEnabled());
}
}
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) {
@@ -686,8 +667,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
copyEntryCode(entry);
}
@Override
@@ -724,12 +703,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
@@ -738,7 +721,10 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
}
@Override
public void onLocked() {
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onLocked(boolean userInitiated) {
if (_actionMode != null) {
_actionMode.finish();
}
@@ -746,18 +732,17 @@ 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) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", entry.getInfo().getOtp());
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.code_copied), Toast.LENGTH_SHORT).show();
}
private class ActionModeCallbacks implements ActionMode.Callback {
@@ -782,24 +767,38 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
case R.id.action_edit:
startEditProfileActivity(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());
}
}
}
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,59 +1,124 @@
package com.beemdevelopment.aegis.ui;
import android.os.Bundle;
import android.view.MenuItem;
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.Theme;
import com.beemdevelopment.aegis.ui.fragments.MainPreferencesFragment;
import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
public class PreferencesActivity extends AegisActivity {
private PreferencesFragment _fragment;
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));
_fragment = new PreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit();
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (savedInstanceState == null) {
_fragment = new MainPreferencesFragment();
_fragment.setArguments(getIntent().getExtras());
getSupportFragmentManager().beginTransaction()
.replace(R.id.content, _fragment)
.commit();
PreferencesFragment requestedFragment = getRequestedFragment();
if (requestedFragment != null) {
_fragment = requestedFragment;
showFragment(_fragment);
}
} else {
_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
protected void setPreferredTheme(Theme theme) {
switch (theme) {
case LIGHT:
setTheme(R.style.AppTheme);
break;
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
} else {
return super.onOptionsItemSelected(item);
}
case DARK:
setTheme(R.style.AppTheme_Dark);
break;
return true;
}
case AMOLED:
setTheme(R.style.AppTheme_TrueBlack_Preferences);
break;
@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,774 +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.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 com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.beemdevelopment.aegis.ViewMode;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.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.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 Intent _result;
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;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
AegisApplication app = (AegisApplication) getActivity().getApplication();
_vault = app.getVaultManager();
// set the result intent in advance
setResult(new Intent());
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
Preference darkModePreference = findPreference("pref_dark_mode");
darkModePreference.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 = app.getPreferences().getCurrentTheme().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_theme)
.setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
dialog.dismiss();
_result.putExtra("needsRecreate", true);
getActivity().recreate();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference langPreference = findPreference("pref_lang");
langPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRecreate", true);
getActivity().recreate();
return true;
});
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Preference viewModePreference = findPreference("pref_view_mode");
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode]));
viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_view_mode)
.setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i]));
_result.putExtra("needsRefresh", true);
dialog.dismiss();
})
.setNegativeButton(android.R.string.cancel, null)
.create());
return true;
}
});
Preference importPreference = findPreference("pref_import");
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
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 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) {
Toast.makeText(getActivity(), R.string.disable_encryption_error, Toast.LENGTH_SHORT).show();
return;
}
// clear the KeyStore
try {
KeyStoreHandle handle = new KeyStoreHandle();
handle.clear();
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
getActivity().stopService(new Intent(getActivity(), NotificationService.class));
updateEncryptionPreferences();
}
})
.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;
}
});
_autoLockPreference = findPreference("pref_auto_lock");
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
updateEncryptionPreferences();
}
@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;
}
}
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) {
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
}
});
}
} else {
importDatabase(state);
}
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show();
}
}
private void onImportDecryptResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
_importerState = null;
return;
}
VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds");
DatabaseImporter.State state;
try {
state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds);
} catch (VaultFileException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.decryption_error, Toast.LENGTH_SHORT).show();
return;
}
importDatabase(state);
_importerState = null;
}
private void onImportResult(int resultCode, Intent data) {
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();
Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show();
}
}
private void importDatabase(DatabaseImporter.State state) {
DatabaseImporter.Result result;
try {
result = state.convert();
} catch (DatabaseImporterException e) {
e.printStackTrace();
String msg = String.format("%s: %s", getString(R.string.parsing_file_error), e.getMessage());
Toast.makeText(getContext(), msg, Toast.LENGTH_LONG).show();
return;
}
_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) -> {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, checked.get() ? VaultManager.FILENAME_EXPORT : VaultManager.FILENAME_EXPORT_PLAIN);
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) {
Toast.makeText(getActivity(), R.string.exporting_vault_error, Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
}
private boolean saveVault() {
try {
_vault.save();
} catch (VaultManagerException e) {
Toast.makeText(getActivity(), R.string.saving_error, Toast.LENGTH_LONG).show();
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);
} else {
_setPasswordPreference.setEnabled(false);
_biometricsPreference.setEnabled(false);
_biometricsPreference.setChecked(false, true);
_slotsPreference.setVisible(false);
}
}
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) {
updateEncryptionPreferences();
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
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) {
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)) {
Toast.makeText(getActivity(), String.format("%s: %s", getString(R.string.encryption_enable_biometrics_error), errString), Toast.LENGTH_LONG).show();
}
}
}
private class EnableEncryptionListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {
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) {
Toast.makeText(getActivity(), getString(R.string.encryption_set_password_error) + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
}
@@ -1,137 +1,140 @@
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.vault.VaultEntry;
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.google.zxing.BarcodeFormat;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.zxing.Result;
import java.util.Collections;
import java.util.ArrayList;
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));
_scannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
return new SquareFinderView(context);
_entries = new ArrayList<>();
_lenses = new ArrayList<>();
_previewView = findViewById(R.id.preview_view);
_executor = Executors.newSingleThreadExecutor();
_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);
}
};
_scannerView.setResultHandler(this);
_scannerView.setFormats(Collections.singletonList(BarcodeFormat.QR_CODE));
int camera = getRearCameraId();
if (camera == -1) {
camera = getFrontCameraId();
if (camera == -1) {
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);
}
@Override
public void onResume() {
super.onResume();
_scannerView.startCamera(getCameraId(_facing));
}
@Override
public void onPause() {
super.onPause();
_scannerView.stopCamera();
}
@Override
public void handleResult(Result rawResult) {
private void addCamera(int lens) {
try {
// parse google auth uri
GoogleAuthInfo info = GoogleAuthInfo.parseUri(rawResult.getText());
VaultEntry entry = new VaultEntry(info);
Intent intent = new Intent();
intent.putExtra("entry", entry);
setResult(RESULT_OK, intent);
finish();
} catch (GoogleAuthInfoException e) {
Toast.makeText(this, getString(R.string.read_qr_error), Toast.LENGTH_SHORT).show();
CameraSelector camera = new CameraSelector.Builder().requireLensFacing(lens).build();
if (_cameraProvider.hasCamera(camera)) {
_lenses.add(lens);
}
} catch (CameraInfoUnavailableException e) {
e.printStackTrace();
}
_scannerView.resumeCameraPreview(this);
}
private void updateCameraIcon() {
if (_menu != null) {
MenuItem item = _menu.findItem(R.id.action_camera);
boolean dual = getFrontCameraId() != -1 && getRearCameraId() != -1;
boolean dual = _lenses.size() > 1;
if (dual) {
switch (_facing) {
case CAMERA_FACING_BACK:
switch (_currentLens) {
case CameraSelector.LENS_FACING_BACK:
item.setIcon(R.drawable.ic_camera_front_24dp);
break;
case CAMERA_FACING_FRONT:
case CameraSelector.LENS_FACING_FRONT:
item.setIcon(R.drawable.ic_camera_rear_24dp);
break;
}
@@ -140,23 +143,87 @@ public class ScannerActivity extends AegisActivity implements ZXingScannerView.R
}
}
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;
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)));
}
}
return -1;
}
private static int getRearCameraId() {
return getCameraId(CAMERA_FACING_BACK);
private void handleUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
List<VaultEntry> entries = new ArrayList<>();
entries.add(new VaultEntry(info));
finish(entries);
}
private static int getFrontCameraId() {
return getCameraId(CAMERA_FACING_FRONT);
private void handleExportUri(Uri uri) throws GoogleAuthInfoException {
GoogleAuthInfo.Export export = GoogleAuthInfo.parseExportUri(uri);
if (_batchId == 0) {
_batchId = export.getBatchId();
}
int batchIndex = export.getBatchIndex();
if (_batchId != export.getBatchId()) {
Toast.makeText(this, R.string.google_qr_export_unrelated, Toast.LENGTH_SHORT).show();
} else if (_batchIndex == -1 || _batchIndex == batchIndex - 1) {
for (GoogleAuthInfo info : export.getEntries()) {
VaultEntry entry = new VaultEntry(info);
_entries.add(entry);
}
_batchIndex = batchIndex;
if (_batchIndex + 1 == export.getBatchSize()) {
finish(_entries);
}
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();
}
}
private void finish(List<VaultEntry> entries) {
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<VaultEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
}
@@ -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);
}
}

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